btc: provider/wallet setup flow

This commit is contained in:
Isaac Visintainer 2021-04-01 15:33:27 -07:00 committed by ixv
parent d32e78799f
commit cc7318aaa9
12 changed files with 571 additions and 15 deletions

View File

@ -69,6 +69,20 @@
++ on-watch
|= pax=path
^- (quip card _this)
:: checking provider permissions before trying to subscribe
:: terrible hack until we have cross-ship scries
::
?: ?=([%permitted @ ~] pax)
:_ this
=/ jon=json
%+ frond:enjs:format
%'providerStatus'
%- pairs:enjs:format
:~ provider+s+(scot %p our.bowl)
permitted+b+(is-whitelisted:hc src.bowl)
==
[%give %fact ~ %json !>(jon)]~
::
?> ?=([%clients *] pax)
?. (is-whitelisted:hc src.bowl)
~& >>> "btc-provider: blocked client {<src.bowl>}"

View File

@ -108,6 +108,17 @@
|= pax=path
^- (unit (unit cage))
?+ pax (on-peek:def pax)
[%x %configured ~]
=/ provider=json
?~ prov ~
[%s (scot %p host.u.prov)]
=/ result=json
%- pairs:enjs:format
:~ [%'provider' provider]
[%'hasWallet' b+?=(^ walts)]
==
``json+!>(result)
::
[%x %scanned ~]
``noun+!>(scanned-wallets)
::
@ -135,11 +146,43 @@
::
%btc-provider-update
(handle-provider-update:hc !<(update:bp q.cage.sign))
::
%json
?> ?=([%permitted @ ~] wire)
=/ who (slav %p i.t.wire)
:_ state
:~ [%give %fact ~[/all] cage.sign]
[%pass wire %agent [who %btc-provider] %leave ~]
==
==
[cards this]
==
::
++ on-watch on-watch:def
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?> ?=([%all ~] path)
=/ provider=json
?~ prov ~
[%s (scot %p host.u.prov)]
=/ cb=(unit sats) current-balance:hc
=/ wallet=json
?~ walts ~
%- pairs:enjs:format
:~ balance+?~(cb ~ (numb:enjs:format u.cb))
==
=/ initial=json
%+ frond:enjs:format
%initial
%- pairs:enjs:format
:~ [%'provider' provider]
[%'hasWallet' b+?=(^ walts)]
[%wallet wallet]
==
:_ this
[%give %fact ~ %json !>(initial)]~
::
++ on-leave on-leave:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
@ -159,6 +202,11 @@
:~ [%pass /set-provider/[(scot %p host.u.prov)] %agent [host.u.prov %btc-provider] %leave ~]
sub-card
==
::
%check-provider
=/ pax /permitted/(scot %p provider.comm)
:_ state
[%pass pax %agent [provider.comm %btc-provider] %watch pax]~
::
%set-current-wallet
(set-curr-xpub xpub.comm)
@ -493,18 +541,22 @@
|= s=status:bp
^- (quip card _state)
|^
?~ prov `state
?. =(host.u.prov src.bowl) `state
?- -.s
%new-block
(on-connected u.prov network.s block.s fee.s `blockhash.s `blockfilter.s)
::
%connected
(on-connected u.prov network.s block.s fee.s ~ ~)
::
%disconnected
`state(prov `u.prov(connected %.n))
==
=^ cards state
?~ prov `state
?. =(host.u.prov src.bowl) `state
?- -.s
%new-block
(on-connected u.prov network.s block.s fee.s `blockhash.s `blockfilter.s)
::
%connected
(on-connected u.prov network.s block.s fee.s ~ ~)
::
%disconnected
`state(prov `u.prov(connected %.n))
==
:_ state
:_ cards
[%give %fact ~[/all] %btc-provider-status !>(s)]
::
++ on-connected
|= $: p=provider
@ -854,4 +906,10 @@
add
`(roll values add)
::
::
++ current-balance
^- (unit sats)
?~ curr-xpub ~
(balance u.curr-xpub)
::
--

View File

@ -0,0 +1,80 @@
/- btc-wallet, btc-provider, bitcoin
|%
++ dejs
=, dejs:format
|%
++ command
|= jon=json
^- command:btc-wallet
%. jon
%- of
:~ set-provider+ship
check-provider+ship
set-current-wallet+so
add-wallet+add-wallet
delete-wallet+so
init-payment+init-payment
broadcast-tx+so
==
::
++ ship (su ;~(pfix sig fed:ag))
::
++ add-wallet
%- ot
:~ xpub+so
fprint+(at [ni ni ~])
scan-to+(mu (at [ni ni ~]))
max-gap+(mu ni)
confs+(mu ni)
==
::
++ init-payment
%- ot
:~ payee+ship
value+ni
feyb+ni
==
--
::
++ enjs
=, enjs:format
|%
++ status
|= sta=status:btc-provider
^- json
%+ frond -.sta
?- -.sta
%connected (connected sta)
%new-block (new-block sta)
%disconnected ~
==
::
++ connected
|= sta=status:btc-provider
?> ?=(%connected -.sta)
%- pairs
:~ network+s+network.sta
block+(numb block.sta)
fee+?~(fee.sta ~ (numb u.fee.sta))
==
::
++ new-block
|= sta=status:btc-provider
?> ?=(%new-block -.sta)
%- pairs
:~ network+s+network.sta
block+(numb block.sta)
fee+?~(fee.sta ~ (numb u.fee.sta))
blockhash+(hexb blockhash.sta)
blockfilter+(hexb blockfilter.sta)
==
::
++ hexb
|= h=hexb:bitcoin
^- json
%- pairs
:~ wid+(numb:enjs wid.h)
dat+s+(scot %ux dat.h)
==
--
--

View File

@ -1,9 +1,11 @@
/- *btc-provider
/+ bitcoin-json
|_ sta=status
++ grad %noun
++ grow
|%
++ noun sta
++ json (status:enjs:bitcoin-json sta)
--
++ grab
|%

View File

@ -0,0 +1,14 @@
/- *btc-wallet
/+ bitcoin-json
|_ com=command
++ grad %noun
++ grow
|%
++ noun com
--
++ grab
|%
++ noun command
++ json command:dejs:bitcoin-json
--
--

View File

@ -13,6 +13,7 @@
::
+$ command
$% [%set-provider provider=ship]
[%check-provider provider=ship]
[%set-current-wallet =xpub]
[%add-wallet =xpub =fprint scan-to=(unit scon) max-gap=(unit @ud) confs=(unit @ud)]
[%delete-wallet =xpub]

View File

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
Input,
} from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob';
export default class ProviderModal extends Component {
constructor(props) {
super(props);
this.state = {
ready: false,
provider: null,
}
this.checkProvider = this.checkProvider.bind(this);
this.submitProvider = this.submitProvider.bind(this);
}
checkProvider(e) {
// TODO: loading states
let provider = e.target.value;
let ready = false;
if (isValidPatp(provider)) {
let command = {
"check-provider": provider
}
this.props.api.btcWalletCommand(command);
}
this.setState({provider, ready});
}
componentDidUpdate(prevProps, prevState){
if (!this.state.ready){
if (this.props.providerPerms[this.state.provider]) {
this.setState({ready: true});
}
}
}
submitProvider(e){
if (this.state.ready){
let command = {
"set-provider": this.state.provider
}
this.props.api.btcWalletCommand(command);
}
}
render() {
let workingNode = (!this.state.ready) ? null :
<Box mt={3}>
<Text fontSize="14px" color="green">
{this.state.provider} is a working provider node
</Text>
</Box>
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="NullIcon" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 1 of 2: Set up Bitcoin Provider Node
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
In order to perform Bitcoin transaction in Landscape, you'll need to set a provider node. A provider node is an urbit which maintains a synced Bitcoin ledger. Learn More
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Provider Node
</Text>
</Box>
<StatelessTextInput
fontSize="14px"
type="text"
name="masterTicket"
placeholder="e.g. ~zod"
autoCapitalize="none"
autoCorrect="off"
mono
backgroundColor={this.state.ready ? "veryLightGreen": null}
color={this.state.ready ? "green": null}
borderColor={this.state.ready ? "green": null}
onChange={this.checkProvider}
/>
{workingNode}
<Button
mt={3}
primary
disabled={!this.state.ready}
children="Set Peer Node"
fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}}
onClick={() => {this.submitProvider(this.state.provider)}}
/>
</Box>
);
}
}

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
import { Box } from '@tlon/indigo-react';
import WalletModal from './walletModal.js'
import ProviderModal from './providerModal.js'
export default class StartupModal extends Component {
constructor(props) {
super(props);
}
render() {
let modal = null;
if (this.props.state.hasWallet && this.props.state.provider) {
return null;
} else if (!this.props.state.provider){
modal =
<ProviderModal
api={this.props.api}
providerPerms={this.props.state.providerPerms}
/>
} else if (!this.props.state.hasWallet){
modal = <WalletModal api={this.props.api}/>
}
return (
<Box
backgroundColor="scales.black20"
left="0px"
top="0px"
width="100%"
height="100%"
position="fixed"
display="flex"
zIndex={10}
justifyContent="center"
alignItems="center"
>
<Box display="flex"
flexDirection="column"
width='400px'
backgroundColor="white"
borderRadius={3}
>
{modal}
</Box>
</Box>
);
}
}

View File

@ -0,0 +1,204 @@
import React, { Component } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
Input,
} from '@tlon/indigo-react';
const kg = require('urbit-key-generation');
const bitcoin = require('bitcoinjs-lib');
const bs58check = require('bs58check')
import { Buffer } from 'buffer';
function xpubToZpub(xpub) {
var data = bs58check.decode(xpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
return bs58check.encode(data);
}
const BTC_DERIVATION_PATH = "m/84'/0'/0'"
const BTC_DERIVATION_TYPE = 'bitcoin'
// const NETWORK = bitcoin.networks.bitcoin
const NETWORK = bitcoin.networks.testnet
export default class WalletModal extends Component {
constructor(props) {
super(props);
this.state = {
mode: 'masterTicket',
masterTicket: '',
xpub: '',
ready: false,
}
this.checkTicket = this.checkTicket.bind(this);
this.checkXPub = this.checkXPub.bind(this);
this.submitMasterTicket = this.submitMasterTicket.bind(this);
this.submitXPub = this.submitXPub.bind(this);
}
checkTicket(e){
// TODO: port over bridge ticket validation logic
let masterTicket = e.target.value;
let ready = (masterTicket.length > 0);
this.setState({masterTicket, ready});
}
checkXPub(e){
let xpub = e.target.value;
let ready = (xpub.length > 0);
this.setState({xpub, ready});
}
submitMasterTicket(ticket){
console.log("ticket", ticket);
const node = kg.deriveNode(
ticket,
BTC_DERIVATION_TYPE,
BTC_DERIVATION_PATH
);
const zpub = xpubToZpub(
bitcoin.bip32.fromPublicKey(
Buffer.from(node.keys.public, 'hex'),
Buffer.from(node.keys.chain, 'hex'),
NETWORK
).toBase58()
);
this.submitXPub(zpub);
}
submitXPub(xpub){
console.log("xpub", xpub);
const command = {
"add-wallet": {
"xpub": xpub,
"fprint": [0, 0],
"scan-to": null,
"max-gap": 8,
"confs": 1
}
}
api.btcWalletCommand(command);
}
render() {
if (this.state.mode === 'masterTicket'){
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="NullIcon" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 2 of 2: Import your extended public key
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
To begin, you'll need to derive an XPub Bitcoin address using your
master ticket. Learn More
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Master Key
</Text>
</Box>
<StatelessTextInput
value={this.state.masterTicket}
fontSize="14px"
type="password"
name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
onChange={this.checkTicket}
/>
<Box mt={3} mb={3}>
<Text fontSize="14px" fontWeight="regular" color="gray"
style={{cursor: "pointer"}}
onClick={() => { this.setState({mode: 'xpub', xpub: ''})}}
>
Manually import your extended public key ->
</Text>
</Box>
<Button
primary
disabled={!this.state.ready}
children="Next Step"
fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}}
onClick={() => {this.submitMasterTicket(this.state.masterTicket)}}
/>
</Box>
);
} else if (this.state.mode === 'xpub') {
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="NullIcon" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 2 of 2: Import your extended public key
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
Visit bridge.urbit.org to obtain your key
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Extended Public Key (XPub)
</Text>
</Box>
<StatelessTextInput
value={this.state.xpub}
fontSize="14px"
type="password"
name="xpub"
autoCapitalize="none"
autoCorrect="off"
onChange={this.checkXPub}
/>
<Row mt={3}>
<Button
primary
color="black"
backgroundColor="veryLightGray"
borderColor="veryLightGray"
children="Cancel"
fontSize="14px"
mr={2}
style={{cursor: "pointer"}}
onClick={() => {this.setState({mode: 'masterTicket', xpub: ''})}}
/>
<Button
primary
disabled={!this.state.ready}
children="Next Step"
fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}}
onClick={() => { this.submitXPub(this.state.xpub) }}
/>
</Row>
</Box>
);
}
}
}

View File

@ -8,7 +8,7 @@ import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import light from './themes/light';
import dark from './themes/dark';
import { Text, Box } from '@tlon/indigo-react';
//import StartupModal from './lib/startupModal.js';
import StartupModal from './lib/startupModal.js';
//import Header from './lib/header.js'
//import Balance from './lib/balance.js'
//import Transactions from './lib/transactions.js'
@ -31,7 +31,7 @@ export class Root extends Component {
return (
<BrowserRouter>
<ThemeProvider theme={light}>
{// <StartupModal api={api} state={this.state}/> }
<StartupModal api={api} state={this.state}/>
<Box display='flex'
flexDirection='column'
position='absolute'

View File

@ -6,6 +6,9 @@ export class InitialReducer {
let data = _.get(json, 'initial', false);
console.log('data', data);
if (data) {
state.provider = data.provider;
state.hasWallet = data.hasWallet;
state.wallet = data.wallet;
}
}
}

View File

@ -3,5 +3,20 @@ import _ from 'lodash';
export class UpdateReducer {
reduce(json, state) {
console.log('update', json);
if (json.providerStatus) {
this.reduceProviderStatus(json.providerStatus, state);
}
if (json.connected) {
this.reduceConnected(json.connected, state);
}
}
reduceProviderStatus(json, state) {
state.providerPerms[json.provider] = json.permitted;
}
reduceConnected(json, state) {
state.provider = true;
}
}