Merge branch 'integration' of github.com:Chia-Network/chia-blockchain into integration

This commit is contained in:
Mariano Sorgente 2020-03-06 13:41:07 +09:00
commit 3f0c59336f
No known key found for this signature in database
GPG Key ID: 0F866338C369278C
12 changed files with 500 additions and 334 deletions

View File

@ -47,6 +47,7 @@ wcwidth==0.1.8
yarl==1.4.2 yarl==1.4.2
zipp==2.0.0 zipp==2.0.0
sortedcontainers==2.1.0 sortedcontainers==2.1.0
websockets==8.1.0
-e lib/chiapos -e lib/chiapos
-e lib/bip158 -e lib/bip158
-e lib/py-setproctitle -e lib/py-setproctitle

View File

@ -81,7 +81,7 @@ class RequestHeader:
@cbor_message @cbor_message
class RespondHeader: class RespondHeader:
header_block: HeaderBlock header_block: HeaderBlock
bip158_filter: Optional[bytes] transactions_filter: Optional[bytes]
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@ -2,7 +2,7 @@ const electron = require('electron')
const app = electron.app const app = electron.app
const BrowserWindow = electron.BrowserWindow const BrowserWindow = electron.BrowserWindow
const path = require('path') const path = require('path')
const WebSocket = require('ws');
/************************************************************* /*************************************************************
* py process * py process
@ -10,7 +10,7 @@ const path = require('path')
const PY_DIST_FOLDER = 'pydist' const PY_DIST_FOLDER = 'pydist'
const PY_FOLDER = 'rpc' const PY_FOLDER = 'rpc'
const PY_MODULE = 'rpc_wallet' // without .py suffix const PY_MODULE = 'websocket_server' // without .py suffix
let pyProc = null let pyProc = null
let pyPort = null let pyPort = null
@ -68,7 +68,14 @@ app.on('will-quit', exitPyProc)
let mainWindow = null let mainWindow = null
const createWindow = () => { const createWindow = () => {
mainWindow = new BrowserWindow({width: 1500, height: 800}) console.log(process.versions)
mainWindow = new BrowserWindow({
width: 1500,
height: 800,
webPreferences: {
nodeIntegration: true
},})
mainWindow.loadURL(require('url').format({ mainWindow.loadURL(require('url').format({
pathname: path.join(__dirname, 'wallet-dark.html'), pathname: path.join(__dirname, 'wallet-dark.html'),
protocol: 'file:', protocol: 'file:',

View File

@ -128,6 +128,11 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
}, },
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -2900,6 +2905,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"requires": {
"async-limiter": "~1.0.0"
}
},
"xmlbuilder": { "xmlbuilder": {
"version": "9.0.7", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",

View File

@ -16,7 +16,8 @@
"dialogs": "^2.0.1", "dialogs": "^2.0.1",
"electron-rebuild": "^1.10.0", "electron-rebuild": "^1.10.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"qrcode": "^1.4.4" "qrcode": "^1.4.4",
"ws": "^6.2.1"
}, },
"devDependencies": { "devDependencies": {
"electron": "^1.8.8", "electron": "^1.8.8",

View File

@ -1,9 +1,11 @@
//import { createRPC } from '@erebos/rpc-http-browser' const host = "ws://127.0.0.1:9256"
const jquery = require('jquery') const jquery = require('jquery')
var QRCode = require('qrcode') var QRCode = require('qrcode')
var canvas = document.getElementById('qr_canvas') var canvas = document.getElementById('qr_canvas')
const Dialogs = require('dialogs') const Dialogs = require('dialogs')
const dialogs = Dialogs() const dialogs = Dialogs()
const WebSocket = require('ws');
var ws = new WebSocket(host);
let send = document.querySelector('#send') let send = document.querySelector('#send')
let new_address = document.querySelector('#new_address') let new_address = document.querySelector('#new_address')
@ -16,100 +18,166 @@ let red_checkmark = "<i class=\"icon ion-md-close-circle-outline red\"></i>"
var myBalance = 0 var myBalance = 0
var myUnconfirmedBalance = 0 var myUnconfirmedBalance = 0
send.addEventListener('click', () => {
puzzlehash = receiver_address.value
amount_value = amount.value
data = {"puzzlehash": puzzlehash, "amount": amount_value}
json_data = JSON.stringify(data)
jquery.ajax({
type: 'POST',
url: 'http://127.0.0.1:9256/send_transaction',
data: json_data,
dataType: 'json'
})
.done(function(response) {
console.log(response)
success = response["success"]
if (!success) {
dialogs.alert("You don\'t have enough chia for this transactions", ok => {
})
return
}
})
.fail(function(data) {
console.log(data)
});
})
new_address.addEventListener('click', () => {
console.log("new address requesting")
get_new_puzzlehash(0)
})
copy.addEventListener("click", () => {
let puzzle_holder = document.querySelector("#puzzle_holder")
puzzle_holder.select();
/* Copy the text inside the text field */
document.execCommand("copy");
})
function sleep(ms) { function sleep(ms) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
} }
async function get_new_puzzlehash(timeout) { function set_callbacks(socket) {
//wait for wallet.py to start up /*
await sleep(timeout) Sets callbacks for socket events
jquery.ajax({ */
type: 'POST',
url: 'http://127.0.0.1:9256/get_next_puzzle_hash', socket.on('open', function open() {
dataType: 'json' var msg = {"command": "start_server"}
ws.send(JSON.stringify(msg));
});
socket.on('message', function incoming(incoming) {
var message = JSON.parse(incoming);
var command = message["command"];
var data = message["data"];
console.log("Received command: " + command);
if (data) {
console.log("Received message data: " + JSON.stringify(data));
}
if (command == "start_server") {
get_new_puzzlehash();
get_transactions();
get_wallet_balance();
} else if (command == "get_next_puzzle_hash") {
get_new_puzzlehash_response(data);
} else if (command == "get_wallet_balance") {
get_wallet_balance_response(data);
} else if (command == "send_transaction") {
send_transaction_response(data);
} else if (command == "get_transactions") {
get_transactions_response(data);
}
});
socket.on('error', function clear() {
console.log("lol");
connect(1);
});
}
set_callbacks(ws);
async function connect(timeout) {
/*
Tries to connect to the host after a timeout
*/
await sleep(timeout);
ws = new WebSocket(host);
set_callbacks(ws);
}
send.addEventListener('click', () => {
/*
Called when send button in ui is pressed.
*/
puzzlehash = receiver_address.value;
amount_value = amount.value;
data = {
"puzzlehash": puzzlehash,
"amount": amount_value
}
request = {
"command": "send_transaction",
"data": data
}
json_data = JSON.stringify(request);
ws.send(json_data);
}) })
.done(function(response) {
console.log(response) function send_transaction_response(response) {
let puzzle_holder = document.querySelector("#puzzle_holder") /*
puzzle_holder.value = response["puzzlehash"] Called when response is received for send_transaction request
*/
success = response["success"];
if (!success) {
dialogs.alert("You don\'t have enough chia for this transactions", ok => {
})
return
}
}
new_address.addEventListener('click', () => {
/*
Called when new address button is pressed.
*/
console.log("new address requesting");
get_new_puzzlehash(0);
})
copy.addEventListener("click", () => {
/*
Called when copy button is pressed
*/
let puzzle_holder = document.querySelector("#puzzle_holder");
puzzle_holder.select();
/* Copy the text inside the text field */
document.execCommand("copy");
})
async function get_new_puzzlehash() {
/*
Sends websocket request for new puzzle_hash
*/
data = {
"command": "get_next_puzzle_hash",
}
json_data = JSON.stringify(data);
ws.send(json_data);
}
function get_new_puzzlehash_response(response) {
/*
Called when response is received for get_new_puzzle_hash request
*/
let puzzle_holder = document.querySelector("#puzzle_holder");
puzzle_holder.value = response["puzzlehash"];
QRCode.toCanvas(canvas, response["puzzlehash"], function (error) { QRCode.toCanvas(canvas, response["puzzlehash"], function (error) {
if (error) console.error(error) if (error) console.error(error)
console.log('success!'); console.log('success!');
}) })
})
.fail(function(data) {
console.log(data)
get_new_puzzlehash(300)
});
} }
async function get_wallet_balance(timeout) { async function get_wallet_balance() {
//wait for wallet.py to start up /*
await sleep(timeout) Sends websocket request to get wallet balance
jquery.ajax({ */
type: 'POST', data = {
url: 'http://127.0.0.1:9256/get_wallet_balance', "command": "get_wallet_balance",
dataType: 'json' }
}) json_data = JSON.stringify(data);
.done(function(response) { ws.send(json_data);
console.log(response)
})
.fail(function(data) {
console.log(data)
get_wallet_balance(1000)
});
} }
async function get_transactions(timeout) { function get_wallet_balance_response(response) {
//wait for wallet.py to start up console.log("update balance");
await sleep(timeout) }
jquery.ajax({
type: 'POST', async function get_transactions() {
url: 'http://127.0.0.1:9256/get_transactions', /*
dataType: 'json' Sends websocket request to get transactions
}) */
.done(function(response) { data = {
console.log(response) "command": "get_transactions",
}
json_data = JSON.stringify(data);
ws.send(json_data);
}
function get_transactions_response(response) {
/*
Called when response is received for get_transactions request
*/
clean_table() clean_table()
for (var i = 0; i < response.txs.length; i++) { for (var i = 0; i < response.txs.length; i++) {
@ -124,20 +192,20 @@ async function get_transactions(timeout) {
var cell_fee = row.insertCell(5); var cell_fee = row.insertCell(5);
//type of transaction //type of transaction
if (tx["incoming"]) { if (tx["incoming"]) {
cell_type.innerHTML = "Incoming" cell_type.innerHTML = "Incoming";
} else { } else {
cell_type.innerHTML = "Outgoing" cell_type.innerHTML = "Outgoing";
} }
// Receiving puzzle hash // Receiving puzzle hash
cell_to.innerHTML = tx["to_puzzle_hash"] cell_to.innerHTML = tx["to_puzzle_hash"];
// Date // Date
var date = new Date(parseInt(tx["created_at_time"]) * 1000); var date = new Date(parseInt(tx["created_at_time"]) * 1000);
cell_date.innerHTML = "" + date cell_date.innerHTML = "" + date;
// Confirmation status // Confirmation status
if (tx["confirmed"]) { if (tx["confirmed"]) {
index = tx["confirmed_block_index"] index = tx["confirmed_block_index"];
cell_status.innerHTML = "Confirmed" + green_checkmark +"</br>" + "Block: " + index; cell_status.innerHTML = "Confirmed" + green_checkmark +"</br>" + "Block: " + index;
} else { } else {
cell_status.innerHTML = "Pending " + red_checkmark; cell_status.innerHTML = "Pending " + red_checkmark;
@ -145,37 +213,8 @@ async function get_transactions(timeout) {
// Amount and Fee // Amount and Fee
cell_amount.innerHTML = tx["amount"]; cell_amount.innerHTML = tx["amount"];
cell_fee.innerHTML = tx["fee_amount"] cell_fee.innerHTML = tx["fee_amount"];
} }
})
.fail(function(data) {
console.log(data)
get_transactions(1000)
});
}
async function get_server_ready(timeout) {
//wait for wallet.py to start up
await sleep(timeout)
jquery.ajax({
type: 'POST',
url: 'http://127.0.0.1:9256/get_server_ready',
dataType: 'json'
})
.done(function(response) {
console.log(response)
success = response["success"]
if (success) {
get_transactions(0)
get_new_puzzlehash(0)
get_wallet_balance(0)
}
})
.fail(function(data) {
console.log(data)
get_server_ready(100)
});
} }
function clean_table() { function clean_table() {
@ -183,6 +222,5 @@ function clean_table() {
table.deleteRow(0); table.deleteRow(0);
} }
} }
clean_table()
get_server_ready(100)
clean_table();

View File

@ -1,160 +0,0 @@
import asyncio
import dataclasses
import json
from typing import Any
from aiohttp import web
from src.server.outbound_message import NodeType
from src.server.server import ChiaServer
from src.types.peer_info import PeerInfo
from src.util.config import load_config
from src.wallet.wallet_node import WalletNode
class EnhancedJSONEncoder(json.JSONEncoder):
"""
Encodes bytes as hex strings with 0x, and converts all dataclasses to json.
"""
def default(self, o: Any):
if dataclasses.is_dataclass(o):
return o.to_json()
elif hasattr(type(o), "__bytes__"):
return f"0x{bytes(o).hex()}"
return super().default(o)
def obj_to_response(o: Any) -> web.Response:
"""
Converts a python object into json.
"""
json_str = json.dumps(o, cls=EnhancedJSONEncoder, sort_keys=True)
return web.Response(body=json_str, content_type="application/json")
class RpcWalletApiHandler:
"""
Implementation of full node RPC API.
Note that this is not the same as the peer protocol, or wallet protocol (which run Chia's
protocol on top of TCP), it's a separate protocol on top of HTTP thats provides easy access
to the full node.
"""
def __init__(self, wallet_node: WalletNode):
self.wallet_node = wallet_node
async def get_next_puzzle_hash(self, request) -> web.Response:
"""
Returns a new puzzlehash
"""
puzzlehash = (await self.wallet_node.wallet.get_new_puzzlehash()).hex()
response = {
"puzzlehash": puzzlehash,
}
return obj_to_response(response)
async def send_transaction(self, request) -> web.Response:
request_data = await request.json()
if "amount" in request_data and "puzzlehash" in request_data:
amount = int(request_data["amount"])
puzzlehash = request_data["puzzlehash"]
tx = await self.wallet_node.wallet.generate_signed_transaction(
amount, puzzlehash
)
if tx is None:
response = {"success": False}
return obj_to_response(response)
await self.wallet_node.wallet.push_transaction(tx)
response = {"success": True}
return obj_to_response(response)
response = {"success": False}
return obj_to_response(response)
async def get_server_ready(self, request) -> web.Response:
response = {"success": True}
return obj_to_response(response)
async def get_transactions(self, request) -> web.Response:
transactions = (
await self.wallet_node.wallet_state_manager.get_all_transactions()
)
response = {"success": True, "txs": transactions}
return obj_to_response(response)
async def get_wallet_balance(self, request) -> web.Response:
balance = await self.wallet_node.wallet.get_confirmed_balance()
pending_balance = await self.wallet_node.wallet.get_unconfirmed_balance()
response = {
"success": True,
"confirmed_wallet_balance": balance,
"unconfirmed_wallet_balance": pending_balance,
}
return obj_to_response(response)
async def start_rpc_server():
"""
Starts an HTTP server with the following RPC methods, to be used by local clients to
query the node.
"""
config = load_config("config.yaml", "wallet")
try:
key_config = load_config("keys.yaml")
except FileNotFoundError:
raise RuntimeError(
"Keys not generated. Run python3 ./scripts/regenerate_keys.py."
)
wallet_node = await WalletNode.create(config, key_config)
server = ChiaServer(9257, wallet_node, NodeType.WALLET)
wallet_node.set_server(server)
full_node_peer = PeerInfo(
config["full_node_peer"]["host"], config["full_node_peer"]["port"]
)
_ = await server.start_server("127.0.0.1", None)
await asyncio.sleep(1)
_ = await server.start_client(full_node_peer, None)
handler = RpcWalletApiHandler(wallet_node)
app = web.Application()
app.add_routes(
[
web.post("/get_next_puzzle_hash", handler.get_next_puzzle_hash),
web.post("/send_transaction", handler.send_transaction),
web.post("/get_server_ready", handler.get_server_ready),
web.post("/get_transactions", handler.get_transactions),
web.post("/get_wallet_balance", handler.get_wallet_balance),
]
)
runner = web.AppRunner(app, access_log=None)
await runner.setup()
site = web.TCPSite(runner, "localhost", 9256)
await site.start()
await server.await_closed()
async def cleanup():
await runner.cleanup()
return cleanup
async def main():
cleanup = await start_rpc_server()
print("start running on {}")
await cleanup()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,169 @@
import asyncio
import dataclasses
import json
import websockets
from typing import Any, Dict
from aiohttp import web
from src.server.outbound_message import NodeType
from src.server.server import ChiaServer
from src.types.peer_info import PeerInfo
from src.util.config import load_config
from src.wallet.wallet_node import WalletNode
class EnhancedJSONEncoder(json.JSONEncoder):
"""
Encodes bytes as hex strings with 0x, and converts all dataclasses to json.
"""
def default(self, o: Any):
if dataclasses.is_dataclass(o):
return o.to_json()
elif hasattr(type(o), "__bytes__"):
return f"0x{bytes(o).hex()}"
return super().default(o)
def obj_to_response(o: Any) -> str:
"""
Converts a python object into json.
"""
json_str = json.dumps(o, cls=EnhancedJSONEncoder, sort_keys=True)
return json_str
def format_response(command: str, response_data: Dict[str, Any]):
"""
Formats the response into standard format used between renderer.js and here
"""
response = {"command": command, "data": response_data}
json_str = obj_to_response(response)
return json_str
class WebSocketServer:
def __init__(self, wallet_node: WalletNode):
self.wallet_node = wallet_node
async def get_next_puzzle_hash(self, websocket, response_api) -> web.Response:
"""
Returns a new puzzlehash
"""
puzzlehash = (await self.wallet_node.wallet.get_new_puzzlehash()).hex()
data = {
"puzzlehash": puzzlehash,
}
await websocket.send(format_response(response_api, data))
async def send_transaction(self, websocket, request, response_api):
if "amount" in request and "puzzlehash" in request:
amount = int(request["amount"])
puzzlehash = request["puzzlehash"]
tx = await self.wallet_node.wallet.generate_signed_transaction(
amount, puzzlehash
)
if tx is None:
data = {"success": False}
return await websocket.send(format_response(response_api, data))
await self.wallet_node.wallet.push_transaction(tx)
data = {"success": True}
return await websocket.send(format_response(response_api, data))
data = {"success": False}
await websocket.send(format_response(response_api, data))
async def server_ready(self, websocket, response_api):
response = {"success": True}
await websocket.send(format_response(response_api, response))
async def get_transactions(self, websocket, response_api):
transactions = (
await self.wallet_node.wallet_state_manager.get_all_transactions()
)
response = {"success": True, "txs": transactions}
await websocket.send(format_response(response_api, response))
async def get_wallet_balance(self, websocket, response_api):
balance = await self.wallet_node.wallet.get_confirmed_balance()
pending_balance = await self.wallet_node.wallet.get_unconfirmed_balance()
response = {
"success": True,
"confirmed_wallet_balance": balance,
"unconfirmed_wallet_balance": pending_balance,
}
await websocket.send(format_response(response_api, response))
async def handle_message(self, websocket, path):
"""
This function gets called when new message is received via websocket.
"""
async for message in websocket:
print(message)
decoded = json.loads(message)
command = decoded["command"]
data = None
if "data" in decoded:
data = decoded["data"]
if command == "start_server":
await self.server_ready(websocket, command)
elif command == "get_wallet_balance":
await self.get_wallet_balance(websocket, command)
elif command == "send_transaction":
await self.send_transaction(websocket, data, command)
elif command == "get_next_puzzle_hash":
await self.get_next_puzzle_hash(websocket, command)
elif command == "get_transactions":
await self.get_transactions(websocket, command)
else:
response = {"error": f"unknown_command {command}"}
await websocket.send(obj_to_response(response))
async def start_websocket_server():
"""
Starts WalletNode, WebSocketServer, and ChiaServer
"""
config = load_config("config.yaml", "wallet")
try:
key_config = load_config("keys.yaml")
except FileNotFoundError:
raise RuntimeError(
"Keys not generated. Run python3 ./scripts/regenerate_keys.py."
)
wallet_node = await WalletNode.create(config, key_config)
handler = WebSocketServer(wallet_node)
server = ChiaServer(9257, wallet_node, NodeType.WALLET)
wallet_node.set_server(server)
full_node_peer = PeerInfo(
config["full_node_peer"]["host"], config["full_node_peer"]["port"]
)
_ = await server.start_server("127.0.0.1", None)
await asyncio.sleep(1)
_ = await server.start_client(full_node_peer, None)
await websockets.serve(handler.handle_message, "localhost", 9256)
await server.await_closed()
async def main():
await start_websocket_server()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,4 +1,4 @@
from typing import Dict, Optional, List, Tuple from typing import Dict, Optional, List, Tuple, Set
import clvm import clvm
from blspy import ExtendedPrivateKey, PublicKey from blspy import ExtendedPrivateKey, PublicKey
import logging import logging
@ -6,6 +6,7 @@ from src.server.outbound_message import OutboundMessage, NodeType, Message, Deli
from src.server.server import ChiaServer from src.server.server import ChiaServer
from src.protocols import full_node_protocol from src.protocols import full_node_protocol
from src.types.hashable.BLSSignature import BLSSignature from src.types.hashable.BLSSignature import BLSSignature
from src.types.hashable.coin import Coin
from src.types.hashable.coin_solution import CoinSolution from src.types.hashable.coin_solution import CoinSolution
from src.types.hashable.program import Program from src.types.hashable.program import Program
from src.types.hashable.spend_bundle import SpendBundle from src.types.hashable.spend_bundle import SpendBundle
@ -42,9 +43,6 @@ class Wallet:
# TODO Don't allow user to send tx until wallet is synced # TODO Don't allow user to send tx until wallet is synced
synced: bool synced: bool
# Queue of SpendBundles that FullNode hasn't acked yet.
send_queue: Dict[bytes32, SpendBundle]
@staticmethod @staticmethod
async def create( async def create(
config: Dict, config: Dict,
@ -101,20 +99,20 @@ class Wallet:
self.server = server self.server = server
def make_solution(self, primaries=None, min_time=0, me=None, consumed=None): def make_solution(self, primaries=None, min_time=0, me=None, consumed=None):
ret = [] condition_list = []
if primaries: if primaries:
for primary in primaries: for primary in primaries:
ret.append( condition_list.append(
make_create_coin_condition(primary["puzzlehash"], primary["amount"]) make_create_coin_condition(primary["puzzlehash"], primary["amount"])
) )
if consumed: if consumed:
for coin in consumed: for coin in consumed:
ret.append(make_assert_coin_consumed_condition(coin)) condition_list.append(make_assert_coin_consumed_condition(coin))
if min_time > 0: if min_time > 0:
ret.append(make_assert_time_exceeds_condition(min_time)) condition_list.append(make_assert_time_exceeds_condition(min_time))
if me: if me:
ret.append(make_assert_my_coin_id_condition(me["id"])) condition_list.append(make_assert_my_coin_id_condition(me["id"]))
return clvm.to_sexp_f([puzzle_for_conditions(ret), []]) return clvm.to_sexp_f([puzzle_for_conditions(condition_list), []])
async def get_keys( async def get_keys(
self, hash: bytes32 self, hash: bytes32
@ -131,20 +129,33 @@ class Wallet:
async def generate_unsigned_transaction( async def generate_unsigned_transaction(
self, amount: int, newpuzzlehash: bytes32, fee: int = 0 self, amount: int, newpuzzlehash: bytes32, fee: int = 0
) -> List[Tuple[Program, CoinSolution]]: ) -> List[Tuple[Program, CoinSolution]]:
utxos = await self.wallet_state_manager.select_coins(amount + fee) """
Generates a unsigned transaction in form of List(Puzzle, Solutions)
"""
utxos: Optional[Set[Coin]] = await self.wallet_state_manager.select_coins(
amount + fee
)
if utxos is None: if utxos is None:
return [] return []
spends: List[Tuple[Program, CoinSolution]] = []
output_created = False
spend_value = sum([coin.amount for coin in utxos]) spend_value = sum([coin.amount for coin in utxos])
change = spend_value - amount - fee change = spend_value - amount - fee
spends: List[Tuple[Program, CoinSolution]] = []
output_created = False
for coin in utxos: for coin in utxos:
# Get keys for puzzle_hash
puzzle_hash = coin.puzzle_hash puzzle_hash = coin.puzzle_hash
maybe = await self.get_keys(puzzle_hash) maybe = await self.get_keys(puzzle_hash)
if not maybe: if not maybe:
return [] return []
# Get puzzle for pubkey
pubkey, secretkey = maybe pubkey, secretkey = maybe
puzzle: Program = puzzle_for_pk(pubkey.serialize()) puzzle: Program = puzzle_for_pk(pubkey.serialize())
# Only one coin creates outputs
if output_created is False: if output_created is False:
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}] primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
if change > 0: if change > 0:
@ -154,29 +165,38 @@ class Wallet:
solution = self.make_solution(primaries=primaries) solution = self.make_solution(primaries=primaries)
output_created = True output_created = True
else: else:
# TODO coin consumed condition should be removed
solution = self.make_solution(consumed=[coin.name()]) solution = self.make_solution(consumed=[coin.name()])
spends.append((puzzle, CoinSolution(coin, solution))) spends.append((puzzle, CoinSolution(coin, solution)))
return spends return spends
async def sign_transaction(self, spends: List[Tuple[Program, CoinSolution]]): async def sign_transaction(self, spends: List[Tuple[Program, CoinSolution]]):
sigs = [] signatures = []
for puzzle, solution in spends: for puzzle, solution in spends:
# Get keys
keys = await self.get_keys(solution.coin.puzzle_hash) keys = await self.get_keys(solution.coin.puzzle_hash)
if not keys: if not keys:
return None return None
pubkey, secretkey = keys pubkey, secretkey = keys
secretkey = BLSPrivateKey(secretkey) secretkey = BLSPrivateKey(secretkey)
code_ = [puzzle, solution.solution] code_ = [puzzle, solution.solution]
sexp = clvm.to_sexp_f(code_) sexp = clvm.to_sexp_f(code_)
# Get AGGSIG conditions
err, con, cost = conditions_for_solution(sexp) err, con, cost = conditions_for_solution(sexp)
if err or not con: if err or not con:
return None return None
conditions_dict = conditions_by_opcode(con) conditions_dict = conditions_by_opcode(con)
for _ in hash_key_pairs_for_conditions_dict(conditions_dict): # Create signature
signature = secretkey.sign(_.message_hash) for pk_message in hash_key_pairs_for_conditions_dict(conditions_dict):
sigs.append(signature) signature = secretkey.sign(pk_message.message_hash)
aggsig = BLSSignature.aggregate(sigs) signatures.append(signature)
# Aggregate signatures
aggsig = BLSSignature.aggregate(signatures)
solution_list: List[CoinSolution] = [ solution_list: List[CoinSolution] = [
CoinSolution( CoinSolution(
coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution]) coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution])
@ -184,6 +204,7 @@ class Wallet:
for (puzzle, coin_solution) in spends for (puzzle, coin_solution) in spends
] ]
spend_bundle = SpendBundle(solution_list, aggsig) spend_bundle = SpendBundle(solution_list, aggsig)
return spend_bundle return spend_bundle
async def generate_signed_transaction( async def generate_signed_transaction(

View File

@ -11,6 +11,7 @@ from src.util.ints import uint32
from src.util.api_decorators import api_request from src.util.api_decorators import api_request
from src.wallet.wallet import Wallet from src.wallet.wallet import Wallet
from src.wallet.wallet_state_manager import WalletStateManager from src.wallet.wallet_state_manager import WalletStateManager
from src.wallet.block_record import BlockRecord
class WalletNode: class WalletNode:
@ -22,6 +23,7 @@ class WalletNode:
log: logging.Logger log: logging.Logger
wallet: Wallet wallet: Wallet
constants: Dict constants: Dict
short_sync_threshold: int
@staticmethod @staticmethod
async def create( async def create(
@ -49,6 +51,7 @@ class WalletNode:
self.wallet = await Wallet.create(config, key_config, self.wallet_state_manager) self.wallet = await Wallet.create(config, key_config, self.wallet_state_manager)
self.server = None self.server = None
self.short_sync_threshold = 10
return self return self
@ -105,7 +108,7 @@ class WalletNode:
if request.weight < lca.weight: if request.weight < lca.weight:
return return
if int(request.height) - int(lca.height) > 10: if int(request.height) - int(lca.height) > self.short_sync_threshold:
try: try:
# Performs sync, and catch exceptions so we don't close the connection # Performs sync, and catch exceptions so we don't close the connection
async for ret_msg in self._sync(): async for ret_msg in self._sync():
@ -126,12 +129,57 @@ class WalletNode:
@api_request @api_request
async def respond_header(self, response: wallet_protocol.RespondHeader): async def respond_header(self, response: wallet_protocol.RespondHeader):
# TODO(mariano): implement block = response.header_block
# 0. If we already have, return # 0. If we already have, return
if block.header_hash in self.wallet_state_manager.block_records:
return
lca = self.wallet_state_manager.block_records[self.wallet_state_manager.lca]
# 1. If disconnected and close, get parent header and return # 1. If disconnected and close, get parent header and return
if block.prev_header_hash not in self.wallet_state_manager.block_records:
if block.height - lca.height < self.short_sync_threshold:
header_request = wallet_protocol.RequestHeader(
uint32(block.height - 1), block.prev_header_hash,
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_header", header_request),
Delivery.RESPOND,
)
return
# 2. If we have transactions, fetch adds/deletes # 2. If we have transactions, fetch adds/deletes
# adds_deletes = await self.wallet_state_manager.filter_additions_removals() if response.transactions_filter is not None:
(
additions,
removals,
) = await self.wallet_state_manager.get_filter_additions_removals(
response.transactions_filter
)
if len(additions) > 0:
request_a = wallet_protocol.RequestAdditions(
block.height, block.header_hash, additions
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_additions", request_a),
Delivery.RESPOND,
)
# Request additions
if len(removals) > 0:
request_r = wallet_protocol.RequestRemovals(
block.height, block.header_hash, removals
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_removals", request_r),
Delivery.RESPOND,
)
else:
block_record = BlockRecord(block.header_hash, block.prev_header_hash, block.height, block.weight, [], [])
res = await self.wallet_state_manager.receive_block(block_record, block)
# 3. If we don't have, don't fetch # 3. If we don't have, don't fetch
# 4. If we have the next header cached, process it # 4. If we have the next header cached, process it
pass pass

View File

@ -365,18 +365,18 @@ class WalletStateManager:
] = await self.wallet_store.get_coin_records_by_spent(False) ] = await self.wallet_store.get_coin_records_by_spent(False)
my_puzzle_hashes = await self.puzzle_store.get_all_puzzle_hashes() my_puzzle_hashes = await self.puzzle_store.get_all_puzzle_hashes()
removals_of_interests: bytes32 = [] removals_of_interest: bytes32 = []
additions_of_interest: bytes32 = [] additions_of_interest: bytes32 = []
for record in my_coin_records: for record in my_coin_records:
if tx_filter.Match(bytearray(record.name())): if tx_filter.Match(bytearray(record.name())):
removals_of_interests.append(record.name()) removals_of_interest.append(record.name())
for puzzle_hash in my_puzzle_hashes: for puzzle_hash in my_puzzle_hashes:
if tx_filter.Match(bytearray(puzzle_hash)): if tx_filter.Match(bytearray(puzzle_hash)):
additions_of_interest.append(puzzle_hash) additions_of_interest.append(puzzle_hash)
return (removals_of_interests, additions_of_interest) return (additions_of_interest, removals_of_interest)
async def get_relevant_additions(self, additions: List[Coin]) -> List[Coin]: async def get_relevant_additions(self, additions: List[Coin]) -> List[Coin]:
""" Returns the list of coins that are relevant to us.(We can spend them) """ """ Returns the list of coins that are relevant to us.(We can spend them) """
@ -407,9 +407,14 @@ class WalletStateManager:
Rolls back and updates the coin_store and transaction store. It's possible this height Rolls back and updates the coin_store and transaction store. It's possible this height
is the tip, or even beyond the tip. is the tip, or even beyond the tip.
""" """
print("Doing reorg...")
await self.wallet_store.rollback_lca_to_block(index) await self.wallet_store.rollback_lca_to_block(index)
# TODO Straya
reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(
index
)
await self.tx_store.rollback_to_block(index)
await self.retry_sending_after_reorg(reorged)
async def retry_sending_after_reorg(self, records: List[TransactionRecord]): async def retry_sending_after_reorg(self, records: List[TransactionRecord]):
""" """

View File

@ -227,3 +227,26 @@ class WalletTransactionStore:
records.append(record) records.append(record)
return records return records
async def get_transaction_above(self, height: uint32) -> List[TransactionRecord]:
cursor = await self.db_connection.execute(
"SELECT * from transaction_record WHERE confirmed_at_index>?", (height,)
)
rows = await cursor.fetchall()
await cursor.close()
records = []
for row in rows:
record = TransactionRecord.from_bytes(row[0])
records.append(record)
return records
async def rollback_to_block(self, block_index):
# Delete from storage
c1 = await self.db_connection.execute(
"DELETE FROM transaction_record WHERE confirmed_at_index>?", (block_index,)
)
await c1.close()
await self.db_connection.commit()