Files
p9/cmd/gui/send.go
2025-11-29 07:24:19 +00:00

493 lines
11 KiB
Go

package gui
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/p9c/p9/pkg/amt"
"github.com/p9c/p9/pkg/btcaddr"
"github.com/atotto/clipboard"
l "github.com/p9c/p9/pkg/gel/gio/layout"
"github.com/p9c/p9/pkg/gel/gio/text"
"github.com/p9c/p9/pkg/gel"
"github.com/p9c/p9/pkg/chainhash"
)
type SendPage struct {
wg *WalletGUI
inputWidth, break1 float32
}
func (wg *WalletGUI) GetSendPage() (sp *SendPage) {
sp = &SendPage{
wg: wg,
inputWidth: 17,
break1: 48,
}
wg.inputs["sendAddress"].SetPasteFunc = sp.pasteFunction
wg.inputs["sendAmount"].SetPasteFunc = sp.pasteFunction
wg.inputs["sendMessage"].SetPasteFunc = sp.pasteFunction
return
}
func (sp *SendPage) Fn(gtx l.Context) l.Dimensions {
wg := sp.wg
return wg.Responsive(
wg.Size.Load(), gel.Widgets{
{
Widget: sp.SmallList,
},
{
Size: sp.break1,
Widget: sp.MediumList,
},
},
).Fn(gtx)
}
func (sp *SendPage) SmallList(gtx l.Context) l.Dimensions {
wg := sp.wg
smallWidgets := []l.Widget{
wg.Flex().Rigid(wg.balanceCard()).Fn,
sp.InputMessage(),
sp.AddressInput(),
sp.AmountInput(),
sp.MessageInput(),
wg.Flex().
Flexed(
1,
sp.SendButton(),
).
Rigid(
wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn,
).
Rigid(
sp.PasteButton(),
).
Rigid(
wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn,
).
Rigid(
sp.SaveButton(),
).Fn,
sp.AddressbookHeader(),
}
smallWidgets = append(smallWidgets, sp.GetAddressbookHistoryCards("DocBg")...)
le := func(gtx l.Context, index int) l.Dimensions {
return wg.Inset(
0.25,
smallWidgets[index],
).Fn(gtx)
}
return wg.lists["send"].
Vertical().
Length(len(smallWidgets)).
ListElement(le).Fn(gtx)
}
func (sp *SendPage) InputMessage() l.Widget {
return sp.wg.Body2("Enter or paste the details for a payment").Alignment(text.Start).Fn
}
func (sp *SendPage) MediumList(gtx l.Context) l.Dimensions {
wg := sp.wg
sendFormWidget := []l.Widget{
wg.balanceCard(),
sp.InputMessage(),
sp.AddressInput(),
sp.AmountInput(),
sp.MessageInput(),
wg.Flex().
Flexed(
1,
sp.SendButton(),
).
Rigid(
wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn,
).
Rigid(
sp.PasteButton(),
).
Rigid(
wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn,
).
Rigid(
sp.SaveButton(),
).Fn,
}
sendLE := func(gtx l.Context, index int) l.Dimensions {
return wg.Inset(0.25, sendFormWidget[index]).Fn(gtx)
}
var historyWidget []l.Widget
historyWidget = append(historyWidget, sp.GetAddressbookHistoryCards("DocBg")...)
historyLE := func(gtx l.Context, index int) l.Dimensions {
return wg.Inset(
0.25,
historyWidget[index],
).Fn(gtx)
}
return wg.Flex().AlignStart().
Rigid(
func(gtx l.Context) l.Dimensions {
gtx.Constraints.Max.X =
int(float32(wg.TextSize) * sp.inputWidth)
// gtx.Constraints.Min.X = int(wg.TextSize.True * sp.inputWidth)
return wg.VFlex().AlignStart().
Rigid(
wg.lists["sendMedium"].
Vertical().
Length(len(sendFormWidget)).
ListElement(sendLE).Fn,
).Fn(gtx)
},
).
// Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
Flexed(
1,
wg.VFlex().AlignStart().
Rigid(
sp.AddressbookHeader(),
).
Rigid(
wg.lists["sendAddresses"].
Vertical().
Length(len(historyWidget)).
ListElement(historyLE).Fn,
).Fn,
).Fn(gtx)
}
func (sp *SendPage) AddressInput() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
return wg.inputs["sendAddress"].Fn(gtx)
}
}
func (sp *SendPage) AmountInput() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
return wg.inputs["sendAmount"].Fn(gtx)
}
}
func (sp *SendPage) MessageInput() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
return wg.inputs["sendMessage"].Fn(gtx)
}
}
func (sp *SendPage) SendButton() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
// Disable input when fields are empty - validation done in click handler
return wg.ButtonLayout(
wg.clickables["sendSend"].
SetClick(
func() {
D.Ln("clicked send button")
go func() {
if wg.WalletAndClientRunning() {
var amount float64
var am amt.Amount
var e error
if amount, e = strconv.ParseFloat(
wg.inputs["sendAmount"].GetText(),
64,
); !E.Chk(e) {
if am, e = amt.NewAmount(amount); E.Chk(e) {
D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e)
// todo: indicate this to the user somehow
return
}
} else {
// todo: indicate this to the user somehow
return
}
var addr btcaddr.Address
if addr, e = btcaddr.Decode(
wg.inputs["sendAddress"].GetText(),
wg.cx.ActiveNet,
); E.Chk(e) {
D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e)
D.Ln("invalid address")
// TODO: indicate this to the user somehow
return
}
if e = wg.WalletClient.WalletPassphrase(wg.cx.Config.WalletPass.V(), 5); E.Chk(e) {
D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e)
return
}
var txid *chainhash.Hash
if txid, e = wg.WalletClient.SendToAddress(addr, am); E.Chk(e) {
// TODO: indicate send failure to user somehow
D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e)
return
}
wg.RecentTransactions(10, "recent")
wg.RecentTransactions(-1, "history")
wg.Invalidate()
D.Ln("transaction successful", txid)
sp.saveForm(txid.String())
select {
case <-time.After(time.Second * 5):
case <-wg.quit:
}
}
}()
},
),
).
Background("Primary").
Embed(
wg.Inset(
0.5,
wg.H6("send").Color("Light").Fn,
).
Fn,
).
Fn(gtx)
}
}
func (sp *SendPage) saveForm(txid string) {
wg := sp.wg
D.Ln("processing form data to save")
amtS := wg.inputs["sendAmount"].GetText()
var e error
var amount float64
if amount, e = strconv.ParseFloat(amtS, 64); E.Chk(e) {
return
}
if amount == 0 {
return
}
var ua amt.Amount
if ua, e = amt.NewAmount(amount); E.Chk(e) {
return
}
msg := wg.inputs["sendMessage"].GetText()
if msg == "" {
return
}
addr := wg.inputs["sendAddress"].GetText()
var ad btcaddr.Address
if ad, e = btcaddr.Decode(addr, wg.cx.ActiveNet); E.Chk(e) {
return
}
wg.State.sendAddresses = append(
wg.State.sendAddresses, AddressEntry{
Address: ad.EncodeAddress(),
Label: msg,
Amount: ua,
Created: time.Now(),
TxID: txid,
},
)
// prevent accidental double clicks recording the same entry again
wg.inputs["sendAmount"].SetText("")
wg.inputs["sendMessage"].SetText("")
wg.inputs["sendAddress"].SetText("")
}
func (sp *SendPage) SaveButton() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
// Disable input when fields are empty - validation done in click handler
return wg.ButtonLayout(
wg.clickables["sendSave"].
SetClick(
func() { sp.saveForm("") },
),
).
Background("Primary").
Embed(
wg.Inset(
0.5,
wg.H6("save").Color("Light").Fn,
).
Fn,
).
Fn(gtx)
}
}
func (sp *SendPage) PasteButton() l.Widget {
return func(gtx l.Context) l.Dimensions {
wg := sp.wg
return wg.ButtonLayout(
wg.clickables["sendFromRequest"].
SetClick(func() { sp.pasteFunction() }),
).
Background("Primary").
Embed(
wg.Inset(
0.5,
wg.H6("paste").Color("Light").Fn,
).
Fn,
).
Fn(gtx)
}
}
func (sp *SendPage) pasteFunction() (b bool) {
wg := sp.wg
D.Ln("clicked paste button")
var urn string
var e error
if urn, e = clipboard.ReadAll(); E.Chk(e) {
return
}
if !strings.HasPrefix(urn, "parallelcoin:") {
if e = clipboard.WriteAll(urn); E.Chk(e) {
}
return
}
split1 := strings.Split(urn, "parallelcoin:")
split2 := strings.Split(split1[1], "?")
addr := split2[0]
var ua btcaddr.Address
if ua, e = btcaddr.Decode(addr, wg.cx.ActiveNet); E.Chk(e) {
return
}
_ = ua
b = true
wg.inputs["sendAddress"].SetText(addr)
if len(split2) <= 1 {
return
}
split3 := strings.Split(split2[1], "&")
for i := range split3 {
var split4 []string
split4 = strings.Split(split3[i], "=")
D.Ln(split4)
if len(split4) > 1 {
switch split4[0] {
case "amount":
wg.inputs["sendAmount"].SetText(split4[1])
// D.Ln("############ amount", split4[1])
case "message", "label":
msg := split4[i]
if len(msg) > 64 {
msg = msg[:64]
}
wg.inputs["sendMessage"].SetText(msg)
// D.Ln("############ message", split4[1])
}
}
}
return
}
func (sp *SendPage) AddressbookHeader() l.Widget {
wg := sp.wg
return wg.Flex().AlignStart().
Rigid(
wg.Inset(
0.25,
wg.H5("Send Address Book").Fn,
).Fn,
).Fn
}
func (sp *SendPage) GetAddressbookHistoryCards(bg string) (widgets []l.Widget) {
wg := sp.wg
avail := len(wg.sendAddressbookClickables)
req := len(wg.State.sendAddresses)
if req > avail {
for i := 0; i < req-avail; i++ {
wg.sendAddressbookClickables = append(wg.sendAddressbookClickables, wg.WidgetPool.GetClickable())
}
}
for x := range wg.State.sendAddresses {
j := x
i := len(wg.State.sendAddresses) - 1 - x
widgets = append(
widgets, func(gtx l.Context) l.Dimensions {
return wg.ButtonLayout(
wg.sendAddressbookClickables[i].SetClick(
func() {
sendText := fmt.Sprintf(
"parallelcoin:%s?amount=%8.8f&message=%s",
wg.State.sendAddresses[i].Address,
wg.State.sendAddresses[i].Amount.ToDUO(),
wg.State.sendAddresses[i].Label,
)
D.Ln("clicked send address list item", j)
if e := clipboard.WriteAll(sendText); E.Chk(e) {
}
},
),
).
Background(bg).
Embed(
wg.Inset(
0.25,
wg.VFlex().AlignStart().
Rigid(
wg.Flex().AlignBaseline().
Rigid(
wg.Caption(wg.State.sendAddresses[i].Address).
Font("go regular").Fn,
).
Flexed(
1,
wg.Body1(wg.State.sendAddresses[i].Amount.String()).
Alignment(text.End).Fn,
).
Fn,
).
Rigid(
wg.Inset(
0.25,
wg.Body1(wg.State.sendAddresses[i].Label).MaxLines(1).Fn,
).Fn,
).
Rigid(
gel.If(
wg.State.sendAddresses[i].TxID != "",
func(ctx l.Context) l.Dimensions {
for j := range wg.txHistoryList {
if wg.txHistoryList[j].TxID == wg.State.sendAddresses[i].TxID {
return wg.Flex().Flexed(
1,
wg.VFlex().
Rigid(
wg.Flex().Flexed(
1,
wg.Caption(wg.State.sendAddresses[i].TxID).MaxLines(1).Fn,
).Fn,
).
Rigid(
wg.Body1(
fmt.Sprint(
"Confirmations: ",
wg.txHistoryList[j].Confirmations,
),
).Fn,
).Fn,
).Fn(gtx)
}
}
return func(ctx l.Context) l.Dimensions { return l.Dimensions{} }(gtx)
},
func(ctx l.Context) l.Dimensions { return l.Dimensions{} },
),
).
Fn,
).
Fn,
).Fn(gtx)
},
)
}
return
}