Merge branch 'integration' of github.com:Chia-Network/chia-blockchain into integration
This commit is contained in:
commit
3f0c59336f
|
@ -47,6 +47,7 @@ wcwidth==0.1.8
|
|||
yarl==1.4.2
|
||||
zipp==2.0.0
|
||||
sortedcontainers==2.1.0
|
||||
websockets==8.1.0
|
||||
-e lib/chiapos
|
||||
-e lib/bip158
|
||||
-e lib/py-setproctitle
|
||||
|
|
|
@ -81,7 +81,7 @@ class RequestHeader:
|
|||
@cbor_message
|
||||
class RespondHeader:
|
||||
header_block: HeaderBlock
|
||||
bip158_filter: Optional[bytes]
|
||||
transactions_filter: Optional[bytes]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
@ -2,7 +2,7 @@ const electron = require('electron')
|
|||
const app = electron.app
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const path = require('path')
|
||||
|
||||
const WebSocket = require('ws');
|
||||
|
||||
/*************************************************************
|
||||
* py process
|
||||
|
@ -10,7 +10,7 @@ const path = require('path')
|
|||
|
||||
const PY_DIST_FOLDER = 'pydist'
|
||||
const PY_FOLDER = 'rpc'
|
||||
const PY_MODULE = 'rpc_wallet' // without .py suffix
|
||||
const PY_MODULE = 'websocket_server' // without .py suffix
|
||||
|
||||
let pyProc = null
|
||||
let pyPort = null
|
||||
|
@ -68,7 +68,14 @@ app.on('will-quit', exitPyProc)
|
|||
let mainWindow = null
|
||||
|
||||
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({
|
||||
pathname: path.join(__dirname, 'wallet-dark.html'),
|
||||
protocol: 'file:',
|
||||
|
|
|
@ -128,6 +128,11 @@
|
|||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"dialogs": "^2.0.1",
|
||||
"electron-rebuild": "^1.10.0",
|
||||
"jquery": "^3.4.1",
|
||||
"qrcode": "^1.4.4"
|
||||
"qrcode": "^1.4.4",
|
||||
"ws": "^6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^1.8.8",
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
//import { createRPC } from '@erebos/rpc-http-browser'
|
||||
const host = "ws://127.0.0.1:9256"
|
||||
const jquery = require('jquery')
|
||||
var QRCode = require('qrcode')
|
||||
var canvas = document.getElementById('qr_canvas')
|
||||
const Dialogs = require('dialogs')
|
||||
const dialogs = Dialogs()
|
||||
const WebSocket = require('ws');
|
||||
var ws = new WebSocket(host);
|
||||
|
||||
let send = document.querySelector('#send')
|
||||
let new_address = document.querySelector('#new_address')
|
||||
|
@ -16,173 +18,209 @@ let red_checkmark = "<i class=\"icon ion-md-close-circle-outline red\"></i>"
|
|||
var myBalance = 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)
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function set_callbacks(socket) {
|
||||
/*
|
||||
Sets callbacks for socket events
|
||||
*/
|
||||
|
||||
socket.on('open', function open() {
|
||||
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);
|
||||
})
|
||||
|
||||
function send_transaction_response(response) {
|
||||
/*
|
||||
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', () => {
|
||||
console.log("new address requesting")
|
||||
get_new_puzzlehash(0)
|
||||
/*
|
||||
Called when new address button is pressed.
|
||||
*/
|
||||
console.log("new address requesting");
|
||||
get_new_puzzlehash(0);
|
||||
})
|
||||
|
||||
copy.addEventListener("click", () => {
|
||||
let puzzle_holder = document.querySelector("#puzzle_holder")
|
||||
/*
|
||||
Called when copy button is pressed
|
||||
*/
|
||||
let puzzle_holder = document.querySelector("#puzzle_holder");
|
||||
puzzle_holder.select();
|
||||
/* Copy the text inside the text field */
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
})
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
async function get_new_puzzlehash(timeout) {
|
||||
//wait for wallet.py to start up
|
||||
await sleep(timeout)
|
||||
jquery.ajax({
|
||||
type: 'POST',
|
||||
url: 'http://127.0.0.1:9256/get_next_puzzle_hash',
|
||||
dataType: 'json'
|
||||
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) {
|
||||
if (error) console.error(error)
|
||||
console.log('success!');
|
||||
})
|
||||
.done(function(response) {
|
||||
console.log(response)
|
||||
let puzzle_holder = document.querySelector("#puzzle_holder")
|
||||
puzzle_holder.value = response["puzzlehash"]
|
||||
QRCode.toCanvas(canvas, response["puzzlehash"], function (error) {
|
||||
if (error) console.error(error)
|
||||
console.log('success!');
|
||||
})
|
||||
})
|
||||
.fail(function(data) {
|
||||
console.log(data)
|
||||
get_new_puzzlehash(300)
|
||||
});
|
||||
}
|
||||
|
||||
async function get_wallet_balance(timeout) {
|
||||
//wait for wallet.py to start up
|
||||
await sleep(timeout)
|
||||
jquery.ajax({
|
||||
type: 'POST',
|
||||
url: 'http://127.0.0.1:9256/get_wallet_balance',
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(response) {
|
||||
console.log(response)
|
||||
})
|
||||
.fail(function(data) {
|
||||
console.log(data)
|
||||
get_wallet_balance(1000)
|
||||
});
|
||||
async function get_wallet_balance() {
|
||||
/*
|
||||
Sends websocket request to get wallet balance
|
||||
*/
|
||||
data = {
|
||||
"command": "get_wallet_balance",
|
||||
}
|
||||
json_data = JSON.stringify(data);
|
||||
ws.send(json_data);
|
||||
}
|
||||
|
||||
async function get_transactions(timeout) {
|
||||
//wait for wallet.py to start up
|
||||
await sleep(timeout)
|
||||
jquery.ajax({
|
||||
type: 'POST',
|
||||
url: 'http://127.0.0.1:9256/get_transactions',
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(response) {
|
||||
console.log(response)
|
||||
clean_table()
|
||||
function get_wallet_balance_response(response) {
|
||||
console.log("update balance");
|
||||
}
|
||||
|
||||
for (var i = 0; i < response.txs.length; i++) {
|
||||
var tx = JSON.parse(response.txs[i]);
|
||||
console.log(tx);
|
||||
var row = table.insertRow(0);
|
||||
var cell_type = row.insertCell(0);
|
||||
var cell_to = row.insertCell(1);
|
||||
var cell_date = row.insertCell(2);
|
||||
var cell_status = row.insertCell(3);
|
||||
var cell_amount = row.insertCell(4);
|
||||
var cell_fee = row.insertCell(5);
|
||||
//type of transaction
|
||||
if (tx["incoming"]) {
|
||||
cell_type.innerHTML = "Incoming"
|
||||
} else {
|
||||
cell_type.innerHTML = "Outgoing"
|
||||
}
|
||||
// Receiving puzzle hash
|
||||
cell_to.innerHTML = tx["to_puzzle_hash"]
|
||||
async function get_transactions() {
|
||||
/*
|
||||
Sends websocket request to get transactions
|
||||
*/
|
||||
data = {
|
||||
"command": "get_transactions",
|
||||
}
|
||||
json_data = JSON.stringify(data);
|
||||
ws.send(json_data);
|
||||
}
|
||||
|
||||
// Date
|
||||
var date = new Date(parseInt(tx["created_at_time"]) * 1000);
|
||||
cell_date.innerHTML = "" + date
|
||||
function get_transactions_response(response) {
|
||||
/*
|
||||
Called when response is received for get_transactions request
|
||||
*/
|
||||
clean_table()
|
||||
|
||||
// Confirmation status
|
||||
if (tx["confirmed"]) {
|
||||
index = tx["confirmed_block_index"]
|
||||
cell_status.innerHTML = "Confirmed" + green_checkmark +"</br>" + "Block: " + index;
|
||||
} else {
|
||||
cell_status.innerHTML = "Pending " + red_checkmark;
|
||||
}
|
||||
for (var i = 0; i < response.txs.length; i++) {
|
||||
var tx = JSON.parse(response.txs[i]);
|
||||
console.log(tx);
|
||||
var row = table.insertRow(0);
|
||||
var cell_type = row.insertCell(0);
|
||||
var cell_to = row.insertCell(1);
|
||||
var cell_date = row.insertCell(2);
|
||||
var cell_status = row.insertCell(3);
|
||||
var cell_amount = row.insertCell(4);
|
||||
var cell_fee = row.insertCell(5);
|
||||
//type of transaction
|
||||
if (tx["incoming"]) {
|
||||
cell_type.innerHTML = "Incoming";
|
||||
} else {
|
||||
cell_type.innerHTML = "Outgoing";
|
||||
}
|
||||
// Receiving puzzle hash
|
||||
cell_to.innerHTML = tx["to_puzzle_hash"];
|
||||
|
||||
// Amount and Fee
|
||||
cell_amount.innerHTML = tx["amount"];
|
||||
cell_fee.innerHTML = tx["fee_amount"]
|
||||
// Date
|
||||
var date = new Date(parseInt(tx["created_at_time"]) * 1000);
|
||||
cell_date.innerHTML = "" + date;
|
||||
|
||||
// Confirmation status
|
||||
if (tx["confirmed"]) {
|
||||
index = tx["confirmed_block_index"];
|
||||
cell_status.innerHTML = "Confirmed" + green_checkmark +"</br>" + "Block: " + index;
|
||||
} else {
|
||||
cell_status.innerHTML = "Pending " + red_checkmark;
|
||||
}
|
||||
|
||||
})
|
||||
.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)
|
||||
});
|
||||
// Amount and Fee
|
||||
cell_amount.innerHTML = tx["amount"];
|
||||
cell_fee.innerHTML = tx["fee_amount"];
|
||||
}
|
||||
}
|
||||
|
||||
function clean_table() {
|
||||
while (table.rows.length > 0) {
|
||||
table.deleteRow(0);
|
||||
table.deleteRow(0);
|
||||
}
|
||||
}
|
||||
clean_table()
|
||||
get_server_ready(100)
|
||||
|
||||
clean_table();
|
||||
|
|
|
@ -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())
|
|
@ -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())
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, Optional, List, Tuple
|
||||
from typing import Dict, Optional, List, Tuple, Set
|
||||
import clvm
|
||||
from blspy import ExtendedPrivateKey, PublicKey
|
||||
import logging
|
||||
|
@ -6,6 +6,7 @@ from src.server.outbound_message import OutboundMessage, NodeType, Message, Deli
|
|||
from src.server.server import ChiaServer
|
||||
from src.protocols import full_node_protocol
|
||||
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.program import Program
|
||||
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
|
||||
synced: bool
|
||||
|
||||
# Queue of SpendBundles that FullNode hasn't acked yet.
|
||||
send_queue: Dict[bytes32, SpendBundle]
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
config: Dict,
|
||||
|
@ -101,20 +99,20 @@ class Wallet:
|
|||
self.server = server
|
||||
|
||||
def make_solution(self, primaries=None, min_time=0, me=None, consumed=None):
|
||||
ret = []
|
||||
condition_list = []
|
||||
if primaries:
|
||||
for primary in primaries:
|
||||
ret.append(
|
||||
condition_list.append(
|
||||
make_create_coin_condition(primary["puzzlehash"], primary["amount"])
|
||||
)
|
||||
if 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:
|
||||
ret.append(make_assert_time_exceeds_condition(min_time))
|
||||
condition_list.append(make_assert_time_exceeds_condition(min_time))
|
||||
if me:
|
||||
ret.append(make_assert_my_coin_id_condition(me["id"]))
|
||||
return clvm.to_sexp_f([puzzle_for_conditions(ret), []])
|
||||
condition_list.append(make_assert_my_coin_id_condition(me["id"]))
|
||||
return clvm.to_sexp_f([puzzle_for_conditions(condition_list), []])
|
||||
|
||||
async def get_keys(
|
||||
self, hash: bytes32
|
||||
|
@ -131,20 +129,33 @@ class Wallet:
|
|||
async def generate_unsigned_transaction(
|
||||
self, amount: int, newpuzzlehash: bytes32, fee: int = 0
|
||||
) -> 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:
|
||||
return []
|
||||
spends: List[Tuple[Program, CoinSolution]] = []
|
||||
output_created = False
|
||||
|
||||
spend_value = sum([coin.amount for coin in utxos])
|
||||
change = spend_value - amount - fee
|
||||
|
||||
spends: List[Tuple[Program, CoinSolution]] = []
|
||||
output_created = False
|
||||
|
||||
for coin in utxos:
|
||||
# Get keys for puzzle_hash
|
||||
puzzle_hash = coin.puzzle_hash
|
||||
maybe = await self.get_keys(puzzle_hash)
|
||||
if not maybe:
|
||||
return []
|
||||
|
||||
# Get puzzle for pubkey
|
||||
pubkey, secretkey = maybe
|
||||
puzzle: Program = puzzle_for_pk(pubkey.serialize())
|
||||
|
||||
# Only one coin creates outputs
|
||||
if output_created is False:
|
||||
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
|
||||
if change > 0:
|
||||
|
@ -154,29 +165,38 @@ class Wallet:
|
|||
solution = self.make_solution(primaries=primaries)
|
||||
output_created = True
|
||||
else:
|
||||
# TODO coin consumed condition should be removed
|
||||
solution = self.make_solution(consumed=[coin.name()])
|
||||
|
||||
spends.append((puzzle, CoinSolution(coin, solution)))
|
||||
return spends
|
||||
|
||||
async def sign_transaction(self, spends: List[Tuple[Program, CoinSolution]]):
|
||||
sigs = []
|
||||
signatures = []
|
||||
for puzzle, solution in spends:
|
||||
# Get keys
|
||||
keys = await self.get_keys(solution.coin.puzzle_hash)
|
||||
if not keys:
|
||||
return None
|
||||
pubkey, secretkey = keys
|
||||
secretkey = BLSPrivateKey(secretkey)
|
||||
|
||||
code_ = [puzzle, solution.solution]
|
||||
sexp = clvm.to_sexp_f(code_)
|
||||
|
||||
# Get AGGSIG conditions
|
||||
err, con, cost = conditions_for_solution(sexp)
|
||||
if err or not con:
|
||||
return None
|
||||
conditions_dict = conditions_by_opcode(con)
|
||||
|
||||
for _ in hash_key_pairs_for_conditions_dict(conditions_dict):
|
||||
signature = secretkey.sign(_.message_hash)
|
||||
sigs.append(signature)
|
||||
aggsig = BLSSignature.aggregate(sigs)
|
||||
# Create signature
|
||||
for pk_message in hash_key_pairs_for_conditions_dict(conditions_dict):
|
||||
signature = secretkey.sign(pk_message.message_hash)
|
||||
signatures.append(signature)
|
||||
|
||||
# Aggregate signatures
|
||||
aggsig = BLSSignature.aggregate(signatures)
|
||||
solution_list: List[CoinSolution] = [
|
||||
CoinSolution(
|
||||
coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution])
|
||||
|
@ -184,6 +204,7 @@ class Wallet:
|
|||
for (puzzle, coin_solution) in spends
|
||||
]
|
||||
spend_bundle = SpendBundle(solution_list, aggsig)
|
||||
|
||||
return spend_bundle
|
||||
|
||||
async def generate_signed_transaction(
|
||||
|
|
|
@ -11,6 +11,7 @@ from src.util.ints import uint32
|
|||
from src.util.api_decorators import api_request
|
||||
from src.wallet.wallet import Wallet
|
||||
from src.wallet.wallet_state_manager import WalletStateManager
|
||||
from src.wallet.block_record import BlockRecord
|
||||
|
||||
|
||||
class WalletNode:
|
||||
|
@ -22,6 +23,7 @@ class WalletNode:
|
|||
log: logging.Logger
|
||||
wallet: Wallet
|
||||
constants: Dict
|
||||
short_sync_threshold: int
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
|
@ -49,6 +51,7 @@ class WalletNode:
|
|||
self.wallet = await Wallet.create(config, key_config, self.wallet_state_manager)
|
||||
|
||||
self.server = None
|
||||
self.short_sync_threshold = 10
|
||||
|
||||
return self
|
||||
|
||||
|
@ -105,7 +108,7 @@ class WalletNode:
|
|||
if request.weight < lca.weight:
|
||||
return
|
||||
|
||||
if int(request.height) - int(lca.height) > 10:
|
||||
if int(request.height) - int(lca.height) > self.short_sync_threshold:
|
||||
try:
|
||||
# Performs sync, and catch exceptions so we don't close the connection
|
||||
async for ret_msg in self._sync():
|
||||
|
@ -126,15 +129,60 @@ class WalletNode:
|
|||
|
||||
@api_request
|
||||
async def respond_header(self, response: wallet_protocol.RespondHeader):
|
||||
# TODO(mariano): implement
|
||||
block = response.header_block
|
||||
# 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
|
||||
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
|
||||
# adds_deletes = await self.wallet_state_manager.filter_additions_removals()
|
||||
# 3. If we don't have, don't fetch
|
||||
# 4. If we have the next header cached, process it
|
||||
pass
|
||||
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
|
||||
# 4. If we have the next header cached, process it
|
||||
pass
|
||||
|
||||
@api_request
|
||||
async def reject_header_request(
|
||||
|
|
|
@ -365,18 +365,18 @@ class WalletStateManager:
|
|||
] = await self.wallet_store.get_coin_records_by_spent(False)
|
||||
my_puzzle_hashes = await self.puzzle_store.get_all_puzzle_hashes()
|
||||
|
||||
removals_of_interests: bytes32 = []
|
||||
removals_of_interest: bytes32 = []
|
||||
additions_of_interest: bytes32 = []
|
||||
|
||||
for record in my_coin_records:
|
||||
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:
|
||||
if tx_filter.Match(bytearray(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]:
|
||||
""" 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
|
||||
is the tip, or even beyond the tip.
|
||||
"""
|
||||
print("Doing reorg...")
|
||||
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]):
|
||||
"""
|
||||
|
|
|
@ -227,3 +227,26 @@ class WalletTransactionStore:
|
|||
records.append(record)
|
||||
|
||||
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()
|
||||
|
|
Loading…
Reference in New Issue