-
Notifications
You must be signed in to change notification settings - Fork 213
/
Program.cs
183 lines (159 loc) · 8.75 KB
/
Program.cs
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
using NBitcoin;
using NBitcoin.RPC;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace MultiSig
{
class Program
{
class Party
{
public Party(Mnemonic mnemonic, string password, KeyPath accountKeyPath)
{
// Note: you could just generate the ExtKey with new ExtKey() and save extKey.GetWif(network) somewhere.
// But saving a mnemonic + password is well known UX
Mnemonic = mnemonic;
PartyName = password; //lazy yes
RootExtKey = mnemonic.DeriveExtKey(password);
AccountExtPubKey = RootExtKey.Derive(accountKeyPath).Neuter();
// The AccountKeyPath should be stored along the AccountExtPubKey
// This is the keypath + the hash of the root hd key.
// During signing, NBitcoin need this information to derive the RootExtKey to the address keypath properly.
AccountKeyPath = new RootedKeyPath(RootExtKey.GetPublicKey().GetHDFingerPrint(), accountKeyPath);
}
public string PartyName;
public Mnemonic Mnemonic;
public ExtPubKey AccountExtPubKey;
public ExtKey RootExtKey;
public RootedKeyPath AccountKeyPath;
}
// We will:
// 1. Create a multi sig wallet of Alice and Bob
// 2. Fund it with 1 BTC
// 3. Send 0.4 BTC to a random address from it
public static async Task Main(string[] args)
{
// Start bitcoind and NBXplorer in regtest:
// * Run "bitcoind -regtest"
// * Run ".\build.ps1", then ".\run.ps1 -regtest" in NBXplorer
var network = Network.RegTest;
var client = CreateNBXClient(network);
// Now let's simulate alice and bob in a 2-2 multisig
var alice = new Party(new Mnemonic(Wordlist.English), "Alice",
new KeyPath("1'/2'/3'"));
var bob = new Party(new Mnemonic(Wordlist.English), "Bob",
new KeyPath("5'/2'/3'"));
Console.WriteLine($"Alice should secretly save '{alice.Mnemonic}', and remember her password 'Alice'");
Console.WriteLine("---");
Console.WriteLine($"Alice should secretly save '{bob.Mnemonic}', and remember her password 'Bob'");
Console.WriteLine("---");
Console.WriteLine($"Alice should share '{alice.AccountExtPubKey.GetWif(network)}' with Bob");
Console.WriteLine("---");
Console.WriteLine($"Bob should share '{bob.AccountExtPubKey.GetWif(network)}' with Alice");
var factory = new DerivationStrategyFactory(network);
var derivationStrategy = factory.CreateMultiSigDerivationStrategy(new[]
{
alice.AccountExtPubKey.GetWif(network),
bob.AccountExtPubKey.GetWif(network)
}, 2, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH });
Console.WriteLine("---");
Console.WriteLine($"The derivation strategy '{derivationStrategy}' represents all the data you need to know to track the multisig wallet");
// NBXplorer will start tracking this wallet.
await client.TrackAsync(derivationStrategy);
// This allow you to get events out of NBXPlorer
var evts = client.CreateLongPollingNotificationSession();
// Now let's fund the wallet
var address1 = (await client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit)).Address;
var rpc = new RPCClient(network);
// If that fail, your bitcoin node need some bitcoins
// bitcoin-cli -regtest getnewaddress
// bitcoin-cli -regtest generatetoaddress 101 <address>
await rpc.SendToAddressAsync(address1, Money.Coins(1.0m));
await WaitTransaction(evts, derivationStrategy);
Console.WriteLine("---");
Console.WriteLine("Sent some money to the multi sig wallet");
Console.WriteLine("---");
// You can list transactions
var txs = await client.GetTransactionsAsync(derivationStrategy);
Console.WriteLine($"Number of unconf transactions: {txs.UnconfirmedTransactions.Transactions.Count}");
Console.WriteLine("---");
var balance = await client.GetBalanceAsync(derivationStrategy);
Console.WriteLine($"Balance: {balance.Unconfirmed}");
Console.WriteLine("---");
var randomDestination = new Key().PubKey.GetAddress(ScriptPubKeyType.Segwit, network);
var psbt = (await client.CreatePSBTAsync(derivationStrategy, new CreatePSBTRequest()
{
Destinations =
{
new CreatePSBTDestination()
{
Destination = randomDestination,
Amount = Money.Coins(0.4m),
SubstractFees = true // We will pay fee by sending to destination a bit less than 0.4 BTC
}
},
FeePreference = new FeePreference()
{
// 10 sat/byte. You can remove this in prod, as it will use bitcoin's core estimation.
ExplicitFeeRate = new FeeRate(10.0m)
}
})).PSBT;
var signedByAlice = Sign(alice, derivationStrategy, psbt);
Console.WriteLine("---");
var signedByBob = Sign (bob, derivationStrategy, psbt);
// OK both have signed
var fullySignedPSBT = signedByAlice.Combine(signedByBob);
fullySignedPSBT.Finalize();
var fullySignedTx = fullySignedPSBT.ExtractTransaction();
await client.BroadcastAsync(fullySignedTx);
// Let's wait NBX receives the tx
await WaitTransaction(evts, derivationStrategy);
balance = await client.GetBalanceAsync(derivationStrategy);
Console.WriteLine($"New balance: {balance.Unconfirmed}");
}
private static PSBT Sign(Party party, DerivationStrategyBase derivationStrategy, PSBT psbt)
{
psbt = psbt.Clone();
// NBXplorer does not have knowledge of the account key path, KeyPath are private information of each peer
// NBXplorer only derive 0/* and 1/* on top of provided account xpubs,
// This mean that the input keypaths in the PSBT are in the form 0/* (as if the account key was the root)
// RebaseKeyPaths modifies the PSBT by adding the AccountKeyPath in prefix of all the keypaths of the PSBT
// Note that this is not necessary to do this if the account key is the same as root key.
// Note that also that you don't have to do this, if you do not pass the account key path in the later SignAll call.
// however, this is best practice to rebase the PSBT before signing.
// If you sign with an offline device (hw wallet), the wallet would need the rebased PSBT.
psbt.RebaseKeyPaths(party.AccountExtPubKey, party.AccountKeyPath);
Console.WriteLine("A PSBT is a data structure with all information for a wallet to sign.");
var spend = psbt.GetBalance(derivationStrategy, party.AccountExtPubKey, party.AccountKeyPath);
Console.WriteLine($"{party.PartyName}, Do you agree to sign this transaction spending {spend}?");
// Ok I sign
psbt.SignAll(derivationStrategy, // What addresses to derive?
party.RootExtKey.Derive(party.AccountKeyPath), // With which account private keys?
party.AccountKeyPath); // What is the keypath of the account private key. If you did not rebased the keypath like before, you can remove this parameter
return psbt;
}
static async Task<NewTransactionEvent> WaitTransaction(LongPollingNotificationSession evts, DerivationStrategyBase derivationStrategy)
{
while (true)
{
var evt = await evts.NextEventAsync();
if (evt is NBXplorer.Models.NewTransactionEvent tx)
{
if (tx.DerivationStrategy == derivationStrategy)
return tx;
}
}
}
private static ExplorerClient CreateNBXClient(Network network)
{
NBXplorerNetworkProvider provider = new NBXplorerNetworkProvider(network.ChainName);
ExplorerClient client = new NBXplorer.ExplorerClient(provider.GetFromCryptoCode(network.NetworkSet.CryptoCode));
return client;
}
}
}