Skip to content

Commit

Permalink
Multiple payout targets (issue #113)
Browse files Browse the repository at this point in the history
  • Loading branch information
raffecat committed Oct 2, 2023
1 parent ef637ca commit a13f6e9
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 51 deletions.
14 changes: 14 additions & 0 deletions migrations/v0.1.1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- SQL Migration from v0.1.0-beta2 to v0.1.1

ALTER TABLE payment DROP COLUMN pay_to;
ALTER TABLE payment DROP COLUMN amount;
ALTER TABLE payment ADD COLUMN total NUMERIC(18,8) NOT NULL;

CREATE TABLE IF NOT EXISTS output (
payment_id INTEGER NOT NULL,
vout INTEGER NOT NULL,
pay_to TEXT NOT NULL,
amount NUMERIC(18,8) NOT NULL,
deduct_fee_percent NUMERIC(18,8) NOT NULL,
PRIMARY KEY (payment_id, vout)
);
58 changes: 30 additions & 28 deletions pkg/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,25 +251,31 @@ func (a API) UpdateAccountSettings(foreignID string, update map[string]interface
return pub, nil
}

func (a API) SendFundsToAddress(foreignID string, amount CoinAmount, payTo Address) (txid string, fee CoinAmount, err error) {
func (a API) SendFundsToAddress(foreignID string, explicitFee CoinAmount, payTo []PayTo) (txid string, fee CoinAmount, err error) {
account, err := a.Store.GetAccount(foreignID)
if err != nil {
return
}
if amount.LessThan(TxnDustLimit) {
return "", ZeroCoins, NewErr(BadRequest, "amount is too small - transaction will be rejected: %s", amount.String())
total := decimal.Zero
for _, pay := range payTo {
total = total.Add(pay.Amount)
if pay.Amount.LessThan(TxnDustLimit) {
return "", ZeroCoins, NewErr(BadRequest, "amount is less than dust limit - transaction will be rejected: %s pay to %s", pay.Amount.String(), pay.PayTo)
}
}
builder, err := NewTxnBuilder(&account, a.Store, a.L1)
if err != nil {
return
}
err = builder.AddUTXOsUpToAmount(amount)
err = builder.AddUTXOsUpToAmount(total)
if err != nil {
return
}
err = builder.AddOutput(payTo, amount)
if err != nil {
return
for _, pay := range payTo {
err = builder.AddOutput(pay.PayTo, pay.Amount)
if err != nil {
return
}
}
err = builder.CalculateFee(ZeroCoins)
if err != nil {
Expand All @@ -283,32 +289,32 @@ func (a API) SendFundsToAddress(foreignID string, amount CoinAmount, payTo Addre
// Create the Payment record up-front.
// Save changes to the Account (NextInternalKey) and address pool.
// Reserve the UTXOs for the payment.
tx, err := a.Store.Begin()
dbtx, err := a.Store.Begin()
if err != nil {
return
}
err = account.UpdatePoolAddresses(tx, a.L1) // we have used an address.
err = account.UpdatePoolAddresses(dbtx, a.L1) // we have used an address.
if err != nil {
tx.Rollback()
dbtx.Rollback()
return
}
err = tx.UpdateAccount(account) // for NextInternalKey (change address)
err = dbtx.UpdateAccount(account) // for NextInternalKey (change address)
if err != nil {
tx.Rollback()
dbtx.Rollback()
return
}
// Create the `payment` row with no txid or paid_height.
payment, err := tx.CreatePayment(account.Address, amount, payTo)
payment, err := dbtx.CreatePayment(account.Address, total, payTo)
if err != nil {
tx.Rollback()
dbtx.Rollback()
return
}
// err = tx.ReserveUTXOsForPayment(payId, builder.GetUTXOs()) // TODO
// if err != nil {
// tx.Rollback()
// return
// }
err = tx.Commit()
err = dbtx.Commit()
if err != nil {
return
}
Expand All @@ -321,16 +327,16 @@ func (a API) SendFundsToAddress(foreignID string, amount CoinAmount, payTo Addre

// Update the Payment with the txid,
// which changes it to "accepted" status (accepted by the network)
tx, err = a.Store.Begin()
dbtx, err = a.Store.Begin()
if err != nil {
return
}
err = tx.UpdatePaymentWithTxID(payment.ID, txid)
err = dbtx.UpdatePaymentWithTxID(payment.ID, txid)
if err != nil {
tx.Rollback()
dbtx.Rollback()
return
}
err = tx.Commit()
err = dbtx.Commit()
if err != nil {
return
}
Expand All @@ -340,7 +346,7 @@ func (a API) SendFundsToAddress(foreignID string, amount CoinAmount, payTo Addre
ForeignID: account.ForeignID,
AccountID: account.Address,
PayTo: payTo,
Amount: amount,
Total: total,
TxID: txid,
}
a.bus.Send(PAYMENT_SENT, msg)
Expand All @@ -360,7 +366,7 @@ func (a API) PayInvoiceFromAccount(invoiceID Address, foreignID string) (txid st
if invoiceAmount.LessThan(TxnDustLimit) {
return "", ZeroCoins, fmt.Errorf("invoice amount is too small - transaction will be rejected: %s", invoiceAmount.String())
}
payTo := invoice.ID // pay-to Address is the ID
payToAddress := invoice.ID // pay-to Address is the ID

// Make a Txn to pay `invoiceAmount` from `account` to `payTo`
builder, err := NewTxnBuilder(&account, a.Store, a.L1)
Expand All @@ -371,7 +377,7 @@ func (a API) PayInvoiceFromAccount(invoiceID Address, foreignID string) (txid st
if err != nil {
return
}
err = builder.AddOutput(payTo, invoiceAmount)
err = builder.AddOutput(payToAddress, invoiceAmount)
if err != nil {
return
}
Expand All @@ -384,11 +390,6 @@ func (a API) PayInvoiceFromAccount(invoiceID Address, foreignID string) (txid st
return
}

// TODO: submit the transaction to Core.
// TODO: store back the account (to save NextInternalKey; also call UpdatePoolAddresses)
// TODO: somehow reserve the UTXOs in the interim (prevent accidental double-spend)
// until ChainTracker sees the Txn in a Block and calls MarkUTXOSpent.

// Create the Payment record up-front.
// Save changes to the Account (NextInternalKey) and address pool.
// Reserve the UTXOs for the payment.
Expand All @@ -407,6 +408,7 @@ func (a API) PayInvoiceFromAccount(invoiceID Address, foreignID string) (txid st
return
}
// Create the `payment` row with no txid or paid_height.
payTo := []PayTo{{Amount: invoiceAmount, PayTo: payToAddress}}
payment, err := tx.CreatePayment(account.Address, invoiceAmount, payTo)
if err != nil {
tx.Rollback()
Expand Down Expand Up @@ -449,7 +451,7 @@ func (a API) PayInvoiceFromAccount(invoiceID Address, foreignID string) (txid st
ForeignID: account.ForeignID,
AccountID: account.Address,
PayTo: payTo,
Amount: invoiceAmount,
Total: invoiceAmount,
TxID: txid,
}
a.bus.Send(PAYMENT_SENT, msg)
Expand Down
4 changes: 2 additions & 2 deletions pkg/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ type PaymentEvent struct {
PaymentID int64 `json:"payment_id"`
AccountID Address `json:"account_id"`
ForeignID string `json:"foreign_id"`
PayTo Address `json:"pay_to"`
Amount CoinAmount `json:"amount"`
PayTo []PayTo `json:"pay_to"`
Total CoinAmount `json:"total"`
TxID string `json:"txid"`
}

Expand Down
18 changes: 15 additions & 3 deletions pkg/payment.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package giga

import "time"
import (
"time"

"github.com/shopspring/decimal"
)

type Payment struct {
ID int64 // incrementing payment number, per account
AccountAddress Address // owner account (source of funds)
PayTo Address // a dogecoin address
Amount CoinAmount // how much was paid
PayTo []PayTo // dogecoin addresses and amounts
Total CoinAmount // total paid (excluding fees)
Created time.Time // when the payment was created
PaidTxID string // TXID of the Transaction that made the payment
PaidHeight int64 // Block Height of the Transaction that made the payment
Expand All @@ -15,3 +19,11 @@ type Payment struct {
ConfirmedEvent time.Time // Time when the confirmed event was sent
UnconfirmedEvent time.Time // Time when the unconfirmed event was sent
}

// Pay an amount to an address
// optional DeductFeePercent deducts a percentage of required fees from each PayTo (should sum to 100)
type PayTo struct {
Amount CoinAmount `json:"amount"`
PayTo Address `json:"to"`
DeductFeePercent decimal.Decimal `json:"deduct_fee_percent"`
}
4 changes: 2 additions & 2 deletions pkg/services/balancekeeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ func (b BalanceKeeper) sendPaymentEvents(tx giga.StoreTransaction, acc *giga.Acc
ForeignID: acc.ForeignID,
AccountID: acc.Address,
PayTo: pay.PayTo,
Amount: pay.Amount,
Total: pay.Total,
TxID: pay.PaidTxID,
}
event := giga.PAYMENT_ON_CHAIN
Expand All @@ -351,7 +351,7 @@ func (b BalanceKeeper) sendPaymentEvents(tx giga.StoreTransaction, acc *giga.Acc
ForeignID: acc.ForeignID,
AccountID: acc.Address,
PayTo: pay.PayTo,
Amount: pay.Amount,
Total: pay.Total,
TxID: pay.PaidTxID,
}
event := giga.PAYMENT_CONFIRMED
Expand Down
2 changes: 1 addition & 1 deletion pkg/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type StoreTransaction interface {

// Store a 'payment' which represents a pay-out to another address from a gigawallet
// managed account.
CreatePayment(account Address, amount CoinAmount, payTo Address) (Payment, error)
CreatePayment(account Address, amount CoinAmount, payTo []PayTo) (Payment, error)

// GetPayment returns the Payment for the given ID
GetPayment(account Address, id int64) (Payment, error)
Expand Down
71 changes: 59 additions & 12 deletions pkg/store/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ CREATE INDEX IF NOT EXISTS invoice_account_i ON invoice (account_address);
CREATE TABLE IF NOT EXISTS payment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_address TEXT NOT NULL,
pay_to TEXT NOT NULL,
amount NUMERIC(18,8) NOT NULL,
created DATETIME NOT NULL,
total NUMERIC(18,8) NOT NULL,
paid_txid TEXT,
paid_height INTEGER,
confirmed_height INTEGER,
Expand All @@ -77,6 +76,15 @@ CREATE INDEX IF NOT EXISTS payment_account_i ON payment (account_address);
CREATE INDEX IF NOT EXISTS payment_txid_i ON payment (paid_txid);
CREATE INDEX IF NOT EXISTS payment_paid_height_i ON payment (paid_height);
CREATE TABLE IF NOT EXISTS output (
payment_id INTEGER NOT NULL,
vout INTEGER NOT NULL,
pay_to TEXT NOT NULL,
amount NUMERIC(18,8) NOT NULL,
deduct_fee_percent NUMERIC(18,8) NOT NULL,
PRIMARY KEY (payment_id, vout)
);
CREATE TABLE IF NOT EXISTS utxo (
txn_id TEXT NOT NULL,
vout INTEGER NOT NULL,
Expand Down Expand Up @@ -412,7 +420,7 @@ func (s SQLiteStore) listInvoicesCommon(tx Queryable, account giga.Address, curs
}

// These must match the row.Scan in scanPayment below.
const payment_select_cols = "id, account_address, pay_to, amount, created, paid_txid, paid_height, confirmed_height, on_chain_event, confirmed_event, unconfirmed_event"
const payment_select_cols = "id, account_address, total, created, paid_txid, paid_height, confirmed_height, on_chain_event, confirmed_event, unconfirmed_event"

func (s SQLiteStore) scanPayment(row Scannable, account giga.Address) (giga.Payment, error) {
var paid_txid sql.NullString
Expand All @@ -422,7 +430,7 @@ func (s SQLiteStore) scanPayment(row Scannable, account giga.Address) (giga.Paym
var confirmed_event sql.NullTime
var unconfirmed_event sql.NullTime
pay := giga.Payment{}
err := row.Scan(&pay.ID, &pay.AccountAddress, &pay.PayTo, &pay.Amount, &pay.Created, &paid_txid, &paid_height, &confirmed_height, &on_chain_event, &confirmed_event, &unconfirmed_event)
err := row.Scan(&pay.ID, &pay.AccountAddress, &pay.Total, &pay.Created, &paid_txid, &paid_height, &confirmed_height, &on_chain_event, &confirmed_event, &unconfirmed_event)
if err == sql.ErrNoRows {
return pay, giga.NewErr(giga.NotFound, "payment not found: %v", account)
}
Expand Down Expand Up @@ -452,8 +460,32 @@ func (s SQLiteStore) scanPayment(row Scannable, account giga.Address) (giga.Paym

var get_payment_sql = fmt.Sprintf("SELECT %s FROM payment WHERE id = $1 AND account_address = $2", payment_select_cols)

func (s SQLiteStore) getPaymentOutputs(tx Queryable, payment_id int64) (result []giga.PayTo, err error) {
rows, err := tx.Query("SELECT pay_to, amount, deduct_fee_percent FROM output WHERE payment_id=$1 ORDER BY vout", payment_id)
if err != nil {
return nil, s.dbErr(err, "getPaymentOutputs: querying PayTo")
}
defer rows.Close()
for rows.Next() {
pay := giga.PayTo{}
err := rows.Scan(&pay.PayTo, &pay.Amount, &pay.DeductFeePercent)
if err != nil {
return nil, s.dbErr(err, "getPaymentOutputs: scanning PayTo")
}
result = append(result, pay)
}
if err = rows.Err(); err != nil { // docs say this check is required!
return nil, s.dbErr(err, "getPaymentOutputs: querying PayTo")
}
return
}

func (s SQLiteStore) getPaymentCommon(tx Queryable, account giga.Address, id int64) (giga.Payment, error) {
return s.scanPayment(tx.QueryRow(get_payment_sql, id, account), account)
pay, err := s.scanPayment(tx.QueryRow(get_payment_sql, id, account), account)
if err == nil {
pay.PayTo, err = s.getPaymentOutputs(tx, pay.ID)
}
return pay, err
}

var list_payments_sql = fmt.Sprintf("SELECT %s FROM payment WHERE account_address = $1 AND id >= $2 ORDER BY id LIMIT $3", payment_select_cols)
Expand All @@ -465,12 +497,16 @@ func (s SQLiteStore) listPaymentsCommon(tx Queryable, account giga.Address, curs
}
defer rows.Close()
for rows.Next() {
p, err := s.scanPayment(rows, account)
pay, err := s.scanPayment(rows, account)
if err != nil {
return nil, 0, err // already s.dbErr
}
items = append(items, p)
next_cursor = p.ID + 1
pay.PayTo, err = s.getPaymentOutputs(tx, pay.ID)
if err != nil {
return nil, 0, err // already s.dbErr
}
items = append(items, pay)
next_cursor = pay.ID + 1
}
if err = rows.Err(); err != nil { // docs say this check is required!
return nil, 0, s.dbErr(err, "ListPayments: querying invoices")
Expand Down Expand Up @@ -596,21 +632,32 @@ func (t SQLiteStoreTransaction) StoreInvoice(inv giga.Invoice) error {
return nil
}

func (t SQLiteStoreTransaction) CreatePayment(accountAddr giga.Address, amount giga.CoinAmount, payTo giga.Address) (giga.Payment, error) {
func (t SQLiteStoreTransaction) CreatePayment(accountAddr giga.Address, total giga.CoinAmount, payTo []giga.PayTo) (giga.Payment, error) {
stmt, err := t.tx.Prepare("INSERT INTO output (payment_id, vout, pay_to, amount, deduct_fee_percent) VALUES ($1,$2,$3,$4,$5)")
if err != nil {
return giga.Payment{}, t.store.dbErr(err, "CreatePayment: preparing insert")
}
defer stmt.Close()
now := time.Now()
row := t.tx.QueryRow(
"insert into payment(account_address, pay_to, amount, created) values($1,$2,$3,$4) returning id",
accountAddr, payTo, amount, now)
accountAddr, payTo, total, now)
var id int64
err := row.Scan(&id)
err = row.Scan(&id)
if err != nil {
return giga.Payment{}, t.store.dbErr(err, "createPayment: insert")
}
for vout, pt := range payTo {
_, err = stmt.Exec(id, vout, pt.PayTo, pt.Amount, pt.DeductFeePercent)
if err != nil {
return giga.Payment{}, t.store.dbErr(err, "CreatePayment: executing insert")
}
}
return giga.Payment{
ID: id,
AccountAddress: accountAddr,
PayTo: payTo,
Amount: amount,
Total: total,
Created: now,
}, nil
}
Expand Down
Loading

0 comments on commit a13f6e9

Please sign in to comment.