-
Notifications
You must be signed in to change notification settings - Fork 1
/
XrpWallet.java
191 lines (169 loc) · 6.5 KB
/
XrpWallet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package app.keyconnect.sdk.wallets;
import app.keyconnect.api.client.model.BlockchainAccountInfo.ChainIdEnum;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.UnsignedInteger;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Optional;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDUtils;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.wallet.UnreadableWalletException;
import org.web3j.crypto.Credentials;
import org.xrpl.xrpl4j.codec.addresses.AddressCodec;
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
import org.xrpl.xrpl4j.codec.addresses.VersionType;
import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec;
import org.xrpl.xrpl4j.keypairs.DefaultKeyPairService;
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.ImmutablePayment.Builder;
import org.xrpl.xrpl4j.model.transactions.Payment;
import org.xrpl.xrpl4j.model.transactions.Transaction;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
import org.xrpl.xrpl4j.wallet.DefaultWalletFactory;
import org.xrpl.xrpl4j.wallet.Wallet;
import org.xrpl.xrpl4j.wallet.WalletFactory;
public class XrpWallet implements BlockchainWallet {
public static final String CHAIN_INDEX = "144";
private final WalletFactory walletFactory = DefaultWalletFactory.getInstance();
private final DeterministicKeyChain chain;
private final String passphrase;
private String name;
private final DeterministicSeed seed;
private Wallet wallet;
/**
* Creates XRP Wallet given a private key
* @param name Name of the wallet
* @param privateKey Private key
*/
public XrpWallet(String name, BigInteger privateKey) {
this.name = name;
this.passphrase = null;
this.chain = null;
this.seed = null;
initWalletFromPrivKey(privateKey);
}
/**
* Creates a standalone XRP Wallet
* @param name Name of the wallet
* @param passphrase Passphrase (salt) to use when generating wallet
*/
public XrpWallet(String name, String passphrase) {
this.name = name;
this.seed = new DeterministicSeed(
new SecureRandom(),
256,
Optional.ofNullable(passphrase).orElse("")
);
this.passphrase = passphrase;
this.chain = DeterministicKeyChain.builder().seed(seed).build();
final DeterministicKey key = chain
.getKeyByPath(HDUtils.parsePath("M/44H/" + CHAIN_INDEX + "H/0H/0/0"), true);
final BigInteger privKey = key.getPrivKey();
initWalletFromPrivKey(privKey);
}
/**
* Recovers a XRP wallet given a passphrase (optional) and mnemonic
* @param name Name of the wallet
* @param passphrase Wallet passphrase (salt) - optional
* @param mnemonic Mnemonic to recover from
*/
public XrpWallet(String name, String passphrase, String mnemonic) {
this.name = name;
this.passphrase = passphrase;
try {
this.seed = new DeterministicSeed(
mnemonic,
null,
Optional.ofNullable(passphrase).orElse(""),
Instant.now().getEpochSecond()
);
} catch (UnreadableWalletException e) {
throw new RuntimeException("Unreadable wallet", e);
}
this.chain = DeterministicKeyChain.builder().seed(seed).build();
final DeterministicKey key = chain
.getKeyByPath(HDUtils.parsePath("M/44H/" + CHAIN_INDEX + "H/0H/0/0"), true);
final BigInteger privKey = key.getPrivKey();
initWalletFromPrivKey(privKey);
}
private void initWalletFromPrivKey(BigInteger privKey) {
final byte[] privateKeyBytes = ArrayUtils.subarray(privKey.toByteArray(), 0, 16);
final String seed = AddressCodec.getInstance()
.encodeSeed(UnsignedByteArray.of(privateKeyBytes), VersionType.SECP256K1);
this.wallet = walletFactory.fromSeed(seed, false);
}
@Override
public String buildPaymentTransaction(String to, BigDecimal valueInXrp, @Nullable BigInteger fee,
long sequence) {
final Optional<BigInteger> feeValue = Optional.ofNullable(fee);
final String destination;
final Optional<String> tag;
if (to.contains(":")) {
final String[] destinationAndTag = to.split(":");
destination = destinationAndTag[0];
tag = Optional.of(destinationAndTag[1]);
} else {
destination = to;
tag = Optional.empty();
}
final Builder paymentBuilder = Payment.builder()
.account(wallet.classicAddress())
.amount(XrpCurrencyAmount.ofXrp(valueInXrp))
.destination(Address.builder().value(destination).build())
.sequence(UnsignedInteger.valueOf(sequence))
.signingPublicKey(wallet.publicKey());
tag.ifPresent(t -> paymentBuilder.destinationTag(UnsignedInteger.valueOf(t)));
// todo get fee from keyconnect API
feeValue.ifPresent(f -> paymentBuilder.fee(XrpCurrencyAmount.ofDrops(fee.longValue())));
final Payment unsignedPayment = paymentBuilder.build();
final ObjectMapper objectMapper = ObjectMapperFactory.create();
try {
final String serialisedPayment = objectMapper.writeValueAsString(unsignedPayment);
final XrplBinaryCodec binaryCodec = new XrplBinaryCodec();
final String encodedHex = binaryCodec.encodeForSigning(serialisedPayment);
final String signature = DefaultKeyPairService.getInstance()
.sign(encodedHex, wallet.privateKey().get());
final Transaction signedTransaction = Payment.builder().from(unsignedPayment)
.transactionSignature(signature).build();
final String signedJson = objectMapper.writeValueAsString(signedTransaction);
return binaryCodec.encode(signedJson);
} catch (JsonProcessingException e) {
throw new RuntimeException("Transaction signing failed", e);
}
}
@Override
public String getAddress() {
return wallet.classicAddress().value();
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Nullable
public String getPassphrase() {
return passphrase;
}
@Nullable
public String getMnemonic() {
if (null != seed && null != seed.getMnemonicCode()) {
return String.join(" ", seed.getMnemonicCode());
}
return null;
}
@Override
public ChainIdEnum getChainId() {
return ChainIdEnum.XRP;
}
}