Skip to content

Commit 8d2c5ed

Browse files
authored
add offline create wallet flow, fix transaction submit expiration bug (#48)
1 parent d6572c5 commit 8d2c5ed

File tree

5 files changed

+322
-63
lines changed

5 files changed

+322
-63
lines changed

bin/vault-create-wallet/main.go

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,186 @@
11
package main
22

33
import (
4+
"encoding/hex"
5+
"encoding/json"
46
"flag"
57
"fmt"
6-
"log"
78
"os"
89

910
"github.com/brave-intl/bat-go/utils/altcurrency"
11+
"github.com/brave-intl/bat-go/utils/formatters"
12+
"github.com/brave-intl/bat-go/utils/httpsignature"
1013
"github.com/brave-intl/bat-go/utils/vaultsigner"
1114
"github.com/brave-intl/bat-go/wallet"
1215
"github.com/brave-intl/bat-go/wallet/provider/uphold"
16+
"github.com/hashicorp/vault/api"
17+
log "github.com/sirupsen/logrus"
18+
"golang.org/x/crypto/ed25519"
1319
)
1420

21+
var flags = flag.NewFlagSet("", flag.ExitOnError)
22+
var verbose = flags.Bool("v", false, "verbose output")
23+
var offline = flags.Bool("offline", false, "operate in multi-step offline mode")
24+
25+
// State contains the current state of the registration
26+
type State struct {
27+
WalletInfo wallet.Info `json:"walletInfo"`
28+
Registration string `json:"registration"`
29+
}
30+
1531
func main() {
16-
log.SetFlags(0)
32+
log.SetFormatter(&formatters.CliFormatter{})
1733

18-
flag.Usage = func() {
34+
flags.Usage = func() {
1935
log.Printf("Create a new wallet backed by vault.\n\n")
2036
log.Printf("Usage:\n\n")
2137
log.Printf(" %s WALLET_NAME\n\n", os.Args[0])
2238
log.Printf(" If a vault keypair exists with name WALLET_NAME, it will be used.\n")
2339
log.Printf(" Otherwise a new vault keypair with that name will be generated.\n\n")
40+
flags.PrintDefaults()
41+
}
42+
err := flags.Parse(os.Args[1:])
43+
if err != nil {
44+
log.Fatalln(err)
45+
}
46+
47+
if *verbose {
48+
log.SetLevel(log.DebugLevel)
2449
}
25-
flag.Parse()
2650

27-
args := flag.Args()
51+
args := flags.Args()
2852
if len(args) != 1 {
2953
log.Printf("ERROR: Must pass a single argument to name generated wallet / keypair\n\n")
30-
flag.Usage()
54+
flags.Usage()
3155
os.Exit(1)
3256
}
3357

3458
name := args[0]
59+
logFile := name + "-registration.json"
60+
61+
var state State
62+
var enc *json.Encoder
3563

36-
var info wallet.Info
37-
info.Provider = "uphold"
38-
info.ProviderID = ""
39-
{
40-
tmp := altcurrency.BAT
41-
info.AltCurrency = &tmp
64+
if *offline {
65+
f, err := os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
66+
if err != nil {
67+
log.Fatalln(err)
68+
}
69+
70+
dec := json.NewDecoder(f)
71+
72+
for dec.More() {
73+
err := dec.Decode(&state)
74+
if err != nil {
75+
log.Fatalln(err)
76+
}
77+
}
78+
79+
enc = json.NewEncoder(f)
4280
}
4381

44-
client, err := vaultsigner.Connect()
45-
if err != nil {
46-
log.Fatalln(err)
82+
if len(state.WalletInfo.PublicKey) == 0 || len(state.Registration) == 0 {
83+
var info wallet.Info
84+
info.Provider = "uphold"
85+
info.ProviderID = ""
86+
{
87+
tmp := altcurrency.BAT
88+
info.AltCurrency = &tmp
89+
}
90+
state.WalletInfo = info
91+
92+
client, err := vaultsigner.Connect()
93+
if err != nil {
94+
log.Fatalln(err)
95+
}
96+
97+
signer, err := vaultsigner.New(client, name)
98+
if err != nil {
99+
log.Fatalln(err)
100+
}
101+
102+
fmt.Printf("Keypair with public key: %s\n", signer)
103+
104+
state.WalletInfo.PublicKey = signer.String()
105+
106+
wallet := &uphold.Wallet{Info: state.WalletInfo, PrivKey: signer, PubKey: signer}
107+
108+
reg, err := wallet.PrepareRegistration(name)
109+
if err != nil {
110+
log.Fatalln(err)
111+
}
112+
state.Registration = reg
113+
114+
if *offline {
115+
err = enc.Encode(state)
116+
if err != nil {
117+
log.Fatalln(err)
118+
}
119+
120+
fmt.Printf("Success, signed registration for wallet \"%s\"\n", name)
121+
fmt.Printf("Please copy %s to the online machine and re-run.\n", logFile)
122+
os.Exit(1)
123+
}
47124
}
48125

49-
signer, err := vaultsigner.New(client, name)
126+
if len(state.WalletInfo.ProviderID) == 0 {
127+
var publicKey httpsignature.Ed25519PubKey
128+
publicKey, err := hex.DecodeString(state.WalletInfo.PublicKey)
129+
if err != nil {
130+
log.Fatalln(err)
131+
}
132+
wallet := uphold.Wallet{Info: state.WalletInfo, PrivKey: ed25519.PrivateKey{}, PubKey: publicKey}
133+
134+
err = wallet.SubmitRegistration(state.Registration)
135+
if err != nil {
136+
log.Fatalln(err)
137+
}
138+
139+
fmt.Printf("Success, registered new keypair and wallet \"%s\"\n", name)
140+
fmt.Printf("Uphold card ID %s\n", wallet.Info.ProviderID)
141+
state.WalletInfo.ProviderID = wallet.Info.ProviderID
142+
143+
depositAddr, err := wallet.CreateCardAddress("ethereum")
144+
if err != nil {
145+
log.Fatalln(err)
146+
}
147+
fmt.Printf("ETH deposit addr: %s\n", depositAddr)
148+
149+
if *offline {
150+
err = enc.Encode(state)
151+
if err != nil {
152+
log.Fatalln(err)
153+
}
154+
155+
fmt.Printf("Please copy %s to the offline machine and re-run.\n", logFile)
156+
os.Exit(1)
157+
}
158+
}
159+
160+
client, err := vaultsigner.Connect()
50161
if err != nil {
51162
log.Fatalln(err)
52163
}
53164

54-
fmt.Printf("Generated keypair with public key: %s\n", signer)
55-
56-
wallet := &uphold.Wallet{Info: info, PrivKey: signer, PubKey: signer}
57-
err = wallet.Register(name)
165+
mounts, err := client.Sys().ListMounts()
58166
if err != nil {
59167
log.Fatalln(err)
60168
}
169+
if _, ok := mounts["wallets/"]; !ok {
170+
// Mount kv secret backend if not already mounted
171+
if err = client.Sys().Mount("wallets", &api.MountInput{
172+
Type: "kv",
173+
}); err != nil {
174+
log.Fatalln(err)
175+
}
176+
}
61177

62-
fmt.Printf("Success, registered new keypair and wallet \"%s\"\n", name)
63-
fmt.Printf("Uphold card ID %s", wallet.Info.ProviderID)
64178
_, err = client.Logical().Write("wallets/"+name, map[string]interface{}{
65-
"providerId": wallet.Info.ProviderID,
179+
"providerId": state.WalletInfo.ProviderID,
66180
})
67181
if err != nil {
68182
log.Fatalln(err)
69183
}
184+
185+
fmt.Printf("Wallet setup complete!\n")
70186
}

settlement/README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export VAULT_ADDR=http://127.0.0.1:8200
1717
gpg -d SHARE.GPG | ./vault-unseal
1818
```
1919

20-
## Running settlement
20+
## Bringing up vault
2121

2222
On the offline computer, in one window run:
2323
```
@@ -29,7 +29,10 @@ In another run:
2929
gpg -d SHARE.GPG | ./vault-unseal
3030
```
3131

32-
You are now ready to transact
32+
## Running settlement
33+
34+
First bring up vault as described above.
35+
3336
```
3437
./vault-sign-settlement -in <SETTLEMENT_REPORT.JSON> <SETTLEMENT_WALLET_CARD_ID>
3538
```
@@ -56,3 +59,30 @@ allow restoring from errors and to avoid duplicate payouts.
5659

5760
Finally upload the "-finished" output file to eyeshade to account for payout
5861
transactions that were made.
62+
63+
## Creating a new offline wallet
64+
65+
On the offline machine, first bring up vault as described above.
66+
67+
Run vault-create-wallet, this will sign the registration and store it into
68+
a local file:
69+
```
70+
vault-create-wallet -offline name-of-new-wallet
71+
```
72+
73+
Copy the created `name-of-new-wallet-registration.json` file to the online
74+
machine.
75+
76+
Re-run vault-create-wallet, this will submit the pre-signed registration:
77+
```
78+
export UPHOLD_ENVIRONMENT=
79+
export UPHOLD_HTTP_PROXY=
80+
export UPHOLD_ACCESS_TOKEN=
81+
vault-create-wallet -offline name-of-new-wallet
82+
```
83+
84+
Finally copy `name-of-new-wallet-registration.json` back to the offline
85+
machine and run vault-create-wallet to record the provider ID in vault:
86+
```
87+
vault-create-wallet -offline name-of-new-wallet
88+
```

settlement/settlement.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,29 @@ func SubmitPreparedTransaction(settlementWallet *uphold.Wallet, settlement *Tran
129129
return nil
130130
}
131131

132-
if len(settlement.ProviderID) > 0 && time.Now().Before(settlement.ValidUntil) {
133-
fmt.Printf("already submitted, skipping submit for channel %s\n", settlement.Channel)
134-
return nil
135-
}
136-
137132
if len(settlement.ProviderID) > 0 {
138-
fmt.Printf("already submitted, but quote has expired for channel %s\n", settlement.Channel)
133+
// first check if the transaction has already been confirmed
134+
upholdInfo, err := settlementWallet.GetTransaction(settlement.ProviderID)
135+
if err == nil {
136+
settlement.Status = upholdInfo.Status
137+
settlement.Currency = upholdInfo.DestCurrency
138+
settlement.Amount = upholdInfo.DestAmount
139+
settlement.TransferFee = upholdInfo.TransferFee
140+
settlement.ExchangeFee = upholdInfo.ExchangeFee
141+
142+
if settlement.IsComplete() {
143+
fmt.Printf("transaction already complete for channel %s\n", settlement.Channel)
144+
return nil
145+
}
146+
} else if wallet.IsNotFound(err) { // unconfirmed transactions appear as "not found"
147+
if time.Now().Before(settlement.ValidUntil) {
148+
return nil
149+
}
150+
151+
fmt.Printf("already submitted, but quote has expired for channel %s\n", settlement.Channel)
152+
} else {
153+
return err
154+
}
139155
}
140156

141157
// post the settlement to uphold but do not confirm it

wallet/provider/uphold/httpsigned.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,40 @@ type HTTPSignedRequest struct {
1818
Body string `json:"octets" valid:"json"`
1919
}
2020

21-
// extract an HTTP request from the encapsulated signed request
22-
func (sr *HTTPSignedRequest) extract() (*httpsignature.Signature, *http.Request, error) {
21+
// extract from the encapsulated signed request
22+
// into the provided HTTP request
23+
// NOTE it intentionally does not set the URL
24+
func (sr *HTTPSignedRequest) extract(r *http.Request) (*httpsignature.Signature, error) {
25+
if r == nil {
26+
return nil, errors.New("r was nil")
27+
}
28+
2329
var s httpsignature.Signature
2430
err := s.UnmarshalText([]byte(sr.Headers["signature"]))
2531
if err != nil {
26-
return nil, nil, err
32+
return nil, err
2733
}
2834

29-
var r http.Request
3035
r.Body = ioutil.NopCloser(bytes.NewBufferString(sr.Body))
31-
r.Header = http.Header{}
36+
if r.Header == nil {
37+
r.Header = http.Header{}
38+
}
3239
for k, v := range sr.Headers {
3340
if !httplex.ValidHeaderFieldName(k) {
34-
return nil, nil, errors.New("invalid encapsulated header name")
41+
return nil, errors.New("invalid encapsulated header name")
3542
}
3643
if !httplex.ValidHeaderFieldValue(v) {
37-
return nil, nil, errors.New("invalid encapsulated header value")
44+
return nil, errors.New("invalid encapsulated header value")
3845
}
3946

4047
if k == httpsignature.RequestTarget {
4148
// TODO implement pseudo-header
42-
return nil, nil, fmt.Errorf("%s pseudo-header not implemented", httpsignature.RequestTarget)
49+
return nil, fmt.Errorf("%s pseudo-header not implemented", httpsignature.RequestTarget)
4350
}
4451

4552
r.Header.Set(k, v)
4653
}
47-
return &s, &r, nil
54+
return &s, nil
4855
}
4956

5057
// encapsulate a signed HTTP request

0 commit comments

Comments
 (0)