Files
2026-03-19 19:26:37 -07:00

196 lines
6.6 KiB
Go

// IMPORTANT: This example impairs an existing loan, which has a 60 second grace period.
// After the 60 seconds pass, this example defaults the loan.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
"github.com/Peersyst/xrpl-go/xrpl/flag"
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
"github.com/Peersyst/xrpl-go/xrpl/queries/ledger"
xrpltime "github.com/Peersyst/xrpl-go/xrpl/time"
"github.com/Peersyst/xrpl-go/xrpl/transaction"
"github.com/Peersyst/xrpl-go/xrpl/wallet"
"github.com/Peersyst/xrpl-go/xrpl/websocket"
wstypes "github.com/Peersyst/xrpl-go/xrpl/websocket/types"
)
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanID := setup["loanID1"].(string)
fmt.Printf("\nLoan broker address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("LoanID: %s\n", loanID)
// Check loan status before impairment ----------------------
fmt.Printf("\n=== Loan Status ===\n\n")
loanStatus, err := client.GetLedgerEntry(&ledger.EntryRequest{
Index: loanID,
LedgerIndex: common.Validated,
})
if err != nil {
panic(err)
}
fmt.Printf("Total Amount Owed: %s TSTUSD.\n", loanStatus.Node["TotalValueOutstanding"])
// Convert Ripple Epoch timestamp to local date and time
nextPaymentDueDate := int64(loanStatus.Node["NextPaymentDueDate"].(float64))
paymentDue := time.UnixMilli(xrpltime.RippleTimeToUnixTime(nextPaymentDueDate))
fmt.Printf("Payment Due Date: %s\n", paymentDue.Local().Format(time.DateTime))
// Prepare LoanManage transaction to impair the loan ----------------------
fmt.Printf("\n=== Preparing LoanManage transaction to impair loan ===\n\n")
loanManageImpair := transaction.LoanManage{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanID: loanID,
}
loanManageImpair.SetLoanImpairFlag()
// Flatten() converts the struct to a map and adds the TransactionType field
flatLoanManageImpair := loanManageImpair.Flatten()
loanManageImpairJSON, _ := json.MarshalIndent(flatLoanManageImpair, "", " ")
fmt.Printf("%s\n", string(loanManageImpairJSON))
// Sign, submit, and wait for impairment validation ----------------------
fmt.Printf("\n=== Submitting LoanManage impairment transaction ===\n\n")
impairResponse, err := client.SubmitTxAndWait(flatLoanManageImpair, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if impairResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to impair loan: %s\n", impairResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan impaired successfully!\n")
// Extract loan impairment info from transaction results ----------------------
var loanNode map[string]any
for _, node := range impairResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "Loan" {
loanNode = node.ModifiedNode.FinalFields
break
}
}
// Check grace period and next payment due date
gracePeriod := int64(loanNode["GracePeriod"].(float64))
nextPaymentDueDate = int64(loanNode["NextPaymentDueDate"].(float64))
defaultTime := nextPaymentDueDate + gracePeriod
paymentDue = time.UnixMilli(xrpltime.RippleTimeToUnixTime(nextPaymentDueDate))
fmt.Printf("New Payment Due Date: %s\n", paymentDue.Local().Format(time.DateTime))
fmt.Printf("Grace Period: %d seconds\n", gracePeriod)
// Convert current time to Ripple Epoch timestamp
currentTime := xrpltime.UnixTimeToRippleTime(time.Now().Unix())
// Add a small buffer (5 seconds) to account for ledger close time
secondsUntilDefault := defaultTime - currentTime + 5
// Countdown until loan can be defaulted ----------------------
fmt.Printf("\n=== Countdown until loan can be defaulted ===\n\n")
for secondsUntilDefault >= 0 {
fmt.Printf("\r%d seconds...", secondsUntilDefault)
time.Sleep(time.Second)
secondsUntilDefault--
}
fmt.Print("\rGrace period expired. Loan can now be defaulted.\n")
// Prepare LoanManage transaction to default the loan ----------------------
fmt.Printf("\n=== Preparing LoanManage transaction to default loan ===\n\n")
loanManageDefault := transaction.LoanManage{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanID: loanID,
}
loanManageDefault.SetLoanDefaultFlag()
flatLoanManageDefault := loanManageDefault.Flatten()
loanManageDefaultJSON, _ := json.MarshalIndent(flatLoanManageDefault, "", " ")
fmt.Printf("%s\n", string(loanManageDefaultJSON))
// Sign, submit, and wait for default validation ----------------------
fmt.Printf("\n=== Submitting LoanManage default transaction ===\n\n")
defaultResponse, err := client.SubmitTxAndWait(flatLoanManageDefault, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if defaultResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to default loan: %s\n", defaultResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan defaulted successfully!\n")
// Verify loan default status from transaction results ----------------------
fmt.Printf("\n=== Checking final loan status ===\n\n")
for _, node := range defaultResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "Loan" {
loanNode = node.ModifiedNode.FinalFields
break
}
}
loanFlags := uint32(loanNode["Flags"].(float64))
// Check which loan flags are set
activeFlags := []string{}
if flag.Contains(loanFlags, transaction.TfLoanDefault) {
activeFlags = append(activeFlags, "tfLoanDefault")
}
if flag.Contains(loanFlags, transaction.TfLoanImpair) {
activeFlags = append(activeFlags, "tfLoanImpair")
}
fmt.Printf("Final loan flags: %v\n", activeFlags)
}