diff --git a/CHANGELOG.md b/CHANGELOG.md index d27abef7..5f18fa7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### xrpl - Adds `PermissionedDomain` ledger entry type (XLS-80d). +- Adds `TokenEscrow` support (XLS-85). + +### Fixed + +- Flatten function in Escrow transaction types for Destination and Owner fields. ## [v0.1.11] diff --git a/examples/permissioned-dex/ws/main.go b/examples/permissioned-dex/ws/main.go index 760130a2..a0855bad 100644 --- a/examples/permissioned-dex/ws/main.go +++ b/examples/permissioned-dex/ws/main.go @@ -354,7 +354,7 @@ func main() { return } - offerNode := txResponse.Tx + offerNode := txResponse.TxJSON fmt.Printf("✅ Offer ledger object retrieved\n") fmt.Printf(" 📊 LedgerEntryType: %v\n", offerNode["LedgerEntryType"]) fmt.Printf(" 🏷️ DomainID: %v\n", offerNode["DomainID"]) diff --git a/examples/token-escrow/rpc/main.go b/examples/token-escrow/rpc/main.go new file mode 100644 index 00000000..c7f691b8 --- /dev/null +++ b/examples/token-escrow/rpc/main.go @@ -0,0 +1,271 @@ +package main + +import ( + "fmt" + "time" + + "github.com/Peersyst/xrpl-go/pkg/crypto" + "github.com/Peersyst/xrpl-go/xrpl/currency" + + "github.com/Peersyst/xrpl-go/xrpl/faucet" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/rpc/types" + rippleTime "github.com/Peersyst/xrpl-go/xrpl/time" + transactions "github.com/Peersyst/xrpl-go/xrpl/transaction" + txnTypes "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + "github.com/Peersyst/xrpl-go/xrpl/wallet" +) + +// safeInt64ToUint32 safely converts int64 to uint32 with bounds checking +func safeInt64ToUint32(value int64) uint32 { + if value < 0 { + return 0 + } + if value > int64(^uint32(0)) { + return ^uint32(0) // max uint32 value + } + return uint32(value) +} + +func main() { + + // + // Configure client + // + fmt.Println("⏳ Setting up client...") + cfg, err := rpc.NewClientConfig( + "https://s.devnet.rippletest.net:51234/", + rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()), + ) + if err != nil { + panic(err) + } + + client := rpc.NewClient(cfg) + fmt.Println("✅ Client configured!") + fmt.Println() + + // Configure wallets + issuerWallet, holderWallet, holderWallet2 := createWallets(client) + + // Configure issuer wallet to allow trust line locking + configureIssuerWallet(client, issuerWallet) + + // Create trust line from holder to issuer + createTrustLine(client, issuerWallet, holderWallet, holderWallet2) + + // Mint token from issuer to holder + mintToken(client, issuerWallet, holderWallet) + + // Create escrow, the holder will escrow 100 tokens to the issuer + offerSequence := createEscrow(client, issuerWallet, holderWallet, holderWallet2) + + // Finish escrow + finishEscrow(client, holderWallet, holderWallet2, offerSequence) +} + +// createWallets configures the issuer and holder wallets. +func createWallets(client *rpc.Client) (issuerWallet, holderWallet, holderWallet2 wallet.Wallet) { + fmt.Println("⏳ Setting up wallets...") + issuerWallet, err := wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating issuer wallet: %s\n", err) + return + } + err = client.FundWallet(&issuerWallet) + if err != nil { + fmt.Printf("❌ Error funding issuer wallet: %s\n", err) + return + } + fmt.Println("💸 Issuer wallet funded!") + + // Holder wallet + holderWallet, err = wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating holder wallet: %s\n", err) + return + } + err = client.FundWallet(&holderWallet) + if err != nil { + fmt.Printf("❌ Error funding holder wallet: %s\n", err) + return + } + fmt.Println("💸 Holder wallet funded!") + + // Holder wallet 2 + holderWallet2, err = wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating holder wallet 2: %s\n", err) + return + } + err = client.FundWallet(&holderWallet2) + if err != nil { + fmt.Printf("❌ Error funding holder wallet 2: %s\n", err) + return + } + fmt.Println("💸 Holder wallet 2 funded!") + + fmt.Println("✅ Wallets setup complete!") + fmt.Println("💳 Issuer wallet:", issuerWallet.ClassicAddress) + fmt.Println("💳 Holder wallet:", holderWallet.ClassicAddress) + fmt.Println("💳 Holder wallet 2:", holderWallet2.ClassicAddress) + fmt.Println() + + return issuerWallet, holderWallet, holderWallet2 +} + +// configureIssuerWallet configures the issuer wallet to allow trust line locking. +func configureIssuerWallet(client *rpc.Client, issuerWallet wallet.Wallet) { + fmt.Println("⏳ Configuring issuer wallet...") + accountSet := &transactions.AccountSet{ + BaseTx: transactions.BaseTx{ + Account: issuerWallet.ClassicAddress, + }, + } + accountSet.SetAsfAllowTrustLineLocking() + accountSetResponse, err := client.SubmitTxAndWait(accountSet.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &issuerWallet, + }) + if err != nil { + fmt.Printf("❌ Error configuring issuer wallet: %s\n", err) + return + } + fmt.Println("✅ Issuer wallet configured!") + fmt.Printf("🌐 Hash: %s\n", accountSetResponse.Hash.String()) + fmt.Println() +} + +// createTrustLine creates a trust line for the holder wallet. +func createTrustLine(client *rpc.Client, issuerWallet, holderWallet, holderWallet2 wallet.Wallet) { + fmt.Println("⏳ Creating trust line for holder wallet...") + trustLine := &transactions.TrustSet{ + BaseTx: transactions.BaseTx{ + Account: holderWallet.ClassicAddress, + }, + LimitAmount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "1000000", + }, + } + trustLine.SetSetNoRippleFlag() + trustLineResponse, err := client.SubmitTxAndWait(trustLine.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet, + }) + if err != nil { + fmt.Printf("❌ Error creating trust line: %s\n", err) + return + } + fmt.Println("✅ Trust line created for holder wallet!") + fmt.Printf("🌐 Hash: %s\n", trustLineResponse.Hash.String()) + fmt.Println() + + fmt.Println("⏳ Creating trust line for holder wallet 2...") + trustLine = &transactions.TrustSet{ + BaseTx: transactions.BaseTx{ + Account: holderWallet2.ClassicAddress, + }, + LimitAmount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "1000000", + }, + } + trustLine.SetSetNoRippleFlag() + trustLineResponse, err = client.SubmitTxAndWait(trustLine.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet2, + }) + if err != nil { + fmt.Printf("❌ Error creating trust line: %s\n", err) + return + } + fmt.Println("✅ Trust line created for holder wallet 2!") + fmt.Printf("🌐 Hash: %s\n", trustLineResponse.Hash.String()) + fmt.Println() +} + +// mintToken mints a token for the holder wallet. +func mintToken(client *rpc.Client, issuerWallet, holderWallet wallet.Wallet) { + fmt.Println("⏳ Minting token to holder wallet...") + token := &transactions.Payment{ + BaseTx: transactions.BaseTx{ + Account: issuerWallet.ClassicAddress, + }, + Destination: holderWallet.ClassicAddress, + Amount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "10000", + }, + } + tokenResponse, err := client.SubmitTxAndWait(token.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &issuerWallet, + }) + if err != nil { + fmt.Printf("❌ Error minting token: %s\n", err) + return + } + fmt.Println("✅ Token minted!") + fmt.Printf("🌐 Hash: %s\n", tokenResponse.Hash.String()) + fmt.Println() +} + +// createEscrow creates an escrow for the holder wallet. +func createEscrow(client *rpc.Client, issuerWallet, holderWallet, holderWallet2 wallet.Wallet) (offerSequence uint32) { + fmt.Println("⏳ Creating escrow...") + escrow := &transactions.EscrowCreate{ + BaseTx: transactions.BaseTx{ + Account: holderWallet.ClassicAddress, + }, + Amount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "100", + }, + Destination: holderWallet2.ClassicAddress, + CancelAfter: safeInt64ToUint32(rippleTime.UnixTimeToRippleTime(time.Now().Unix()) + 4000), + FinishAfter: safeInt64ToUint32(rippleTime.UnixTimeToRippleTime(time.Now().Unix() + 5)), + } + escrowResponse, err := client.SubmitTxAndWait(escrow.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet, + }) + if err != nil { + fmt.Printf("❌ Error creating escrow: %s\n", err) + return + } + fmt.Println("✅ Escrow created!") + fmt.Printf("🌐 Hash: %s\n", escrowResponse.Hash.String()) + fmt.Printf("🌐 Sequence: %d\n", escrowResponse.TxJSON.Sequence()) + fmt.Println() + + return escrowResponse.TxJSON.Sequence() + +} + +// finishEscrow finishes the escrow for the holder wallet 2. +func finishEscrow(client *rpc.Client, holderWallet, holderWallet2 wallet.Wallet, offerSequence uint32) { + fmt.Println("⏳ Finishing escrow...") + escrow := &transactions.EscrowFinish{ + BaseTx: transactions.BaseTx{ + Account: holderWallet2.ClassicAddress, + }, + Owner: holderWallet.ClassicAddress, + OfferSequence: offerSequence, + } + escrowResponse, err := client.SubmitTxAndWait(escrow.Flatten(), &types.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet2, + }) + if err != nil { + fmt.Printf("❌ Error finishing escrow: %s\n", err) + return + } + fmt.Println("✅ Escrow finished!") + fmt.Printf("🌐 Hash: %s\n", escrowResponse.Hash.String()) + fmt.Println() +} diff --git a/examples/token-escrow/ws/main.go b/examples/token-escrow/ws/main.go new file mode 100644 index 00000000..e8475f11 --- /dev/null +++ b/examples/token-escrow/ws/main.go @@ -0,0 +1,283 @@ +package main + +import ( + "fmt" + "time" + + "github.com/Peersyst/xrpl-go/pkg/crypto" + "github.com/Peersyst/xrpl-go/xrpl/currency" + "github.com/Peersyst/xrpl-go/xrpl/websocket" + + "github.com/Peersyst/xrpl-go/xrpl/faucet" + rippleTime "github.com/Peersyst/xrpl-go/xrpl/time" + transactions "github.com/Peersyst/xrpl-go/xrpl/transaction" + txnTypes "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + "github.com/Peersyst/xrpl-go/xrpl/wallet" + wstypes "github.com/Peersyst/xrpl-go/xrpl/websocket/types" +) + +// safeInt64ToUint32 safely converts int64 to uint32 with bounds checking +func safeInt64ToUint32(value int64) uint32 { + if value < 0 { + return 0 + } + if value > int64(^uint32(0)) { + return ^uint32(0) // max uint32 value + } + return uint32(value) +} + +func main() { + + // + // Configure client + // + fmt.Println("⏳ Setting up client...") + client := websocket.NewClient( + websocket.NewClientConfig(). + WithHost("wss://s.devnet.rippletest.net:51233"). + WithFaucetProvider(faucet.NewDevnetFaucetProvider()), + ) + + defer func() { + if err := client.Disconnect(); err != nil { + fmt.Println("Error disconnecting:", err) + } + }() + + fmt.Println("✅ Client configured!") + fmt.Println() + + fmt.Println("Connecting to server...") + if err := client.Connect(); err != nil { + fmt.Println(err) + return + } + + fmt.Println("Connection: ", client.IsConnected()) + fmt.Println() + + // Configure wallets + issuerWallet, holderWallet, holderWallet2 := createWallets(client) + + // Configure issuer wallet to allow trust line locking + configureIssuerWallet(client, issuerWallet) + + // Create trust line from holder to issuer + createTrustLine(client, issuerWallet, holderWallet, holderWallet2) + + // Mint token from issuer to holder + mintToken(client, issuerWallet, holderWallet) + + // Create escrow, the holder will escrow 100 tokens to the issuer + offerSequence := createEscrow(client, issuerWallet, holderWallet, holderWallet2) + + // Finish escrow + finishEscrow(client, holderWallet, holderWallet2, offerSequence) +} + +// createWallets configures the issuer and holder wallets. +func createWallets(client *websocket.Client) (issuerWallet, holderWallet, holderWallet2 wallet.Wallet) { + fmt.Println("⏳ Setting up wallets...") + issuerWallet, err := wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating issuer wallet: %s\n", err) + return + } + err = client.FundWallet(&issuerWallet) + if err != nil { + fmt.Printf("❌ Error funding issuer wallet: %s\n", err) + return + } + fmt.Println("💸 Issuer wallet funded!") + + // Holder wallet + holderWallet, err = wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating holder wallet: %s\n", err) + return + } + err = client.FundWallet(&holderWallet) + if err != nil { + fmt.Printf("❌ Error funding holder wallet: %s\n", err) + return + } + fmt.Println("💸 Holder wallet funded!") + + // Holder wallet 2 + holderWallet2, err = wallet.New(crypto.ED25519()) + if err != nil { + fmt.Printf("❌ Error creating holder wallet 2: %s\n", err) + return + } + err = client.FundWallet(&holderWallet2) + if err != nil { + fmt.Printf("❌ Error funding holder wallet 2: %s\n", err) + return + } + fmt.Println("💸 Holder wallet 2 funded!") + + fmt.Println("✅ Wallets setup complete!") + fmt.Println("💳 Issuer wallet:", issuerWallet.ClassicAddress) + fmt.Println("💳 Holder wallet:", holderWallet.ClassicAddress) + fmt.Println("💳 Holder wallet 2:", holderWallet2.ClassicAddress) + fmt.Println() + + return issuerWallet, holderWallet, holderWallet2 +} + +// configureIssuerWallet configures the issuer wallet to allow trust line locking. +func configureIssuerWallet(client *websocket.Client, issuerWallet wallet.Wallet) { + fmt.Println("⏳ Configuring issuer wallet...") + accountSet := &transactions.AccountSet{ + BaseTx: transactions.BaseTx{ + Account: issuerWallet.ClassicAddress, + }, + } + accountSet.SetAsfAllowTrustLineLocking() + accountSetResponse, err := client.SubmitTxAndWait(accountSet.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &issuerWallet, + }) + if err != nil { + fmt.Printf("❌ Error configuring issuer wallet: %s\n", err) + return + } + fmt.Println("✅ Issuer wallet configured!") + fmt.Printf("🌐 Hash: %s\n", accountSetResponse.Hash.String()) + fmt.Println() +} + +// createTrustLine creates a trust line for the holder wallet. +func createTrustLine(client *websocket.Client, issuerWallet, holderWallet, holderWallet2 wallet.Wallet) { + fmt.Println("⏳ Creating trust line for holder wallet...") + trustLine := &transactions.TrustSet{ + BaseTx: transactions.BaseTx{ + Account: holderWallet.ClassicAddress, + }, + LimitAmount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "1000000", + }, + } + trustLine.SetSetNoRippleFlag() + trustLineResponse, err := client.SubmitTxAndWait(trustLine.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet, + }) + if err != nil { + fmt.Printf("❌ Error creating trust line: %s\n", err) + return + } + fmt.Println("✅ Trust line created for holder wallet!") + fmt.Printf("🌐 Hash: %s\n", trustLineResponse.Hash.String()) + fmt.Println() + + fmt.Println("⏳ Creating trust line for holder wallet 2...") + trustLine = &transactions.TrustSet{ + BaseTx: transactions.BaseTx{ + Account: holderWallet2.ClassicAddress, + }, + LimitAmount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "1000000", + }, + } + trustLine.SetSetNoRippleFlag() + trustLineResponse, err = client.SubmitTxAndWait(trustLine.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet2, + }) + if err != nil { + fmt.Printf("❌ Error creating trust line: %s\n", err) + return + } + fmt.Println("✅ Trust line created for holder wallet 2!") + fmt.Printf("🌐 Hash: %s\n", trustLineResponse.Hash.String()) + fmt.Println() +} + +// mintToken mints a token for the holder wallet. +func mintToken(client *websocket.Client, issuerWallet, holderWallet wallet.Wallet) { + fmt.Println("⏳ Minting token to holder wallet...") + token := &transactions.Payment{ + BaseTx: transactions.BaseTx{ + Account: issuerWallet.ClassicAddress, + }, + Destination: holderWallet.ClassicAddress, + Amount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "10000", + }, + } + tokenResponse, err := client.SubmitTxAndWait(token.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &issuerWallet, + }) + if err != nil { + fmt.Printf("❌ Error minting token: %s\n", err) + return + } + fmt.Println("✅ Token minted!") + fmt.Printf("🌐 Hash: %s\n", tokenResponse.Hash.String()) + fmt.Println() +} + +// createEscrow creates an escrow for the holder wallet. +func createEscrow(client *websocket.Client, issuerWallet, holderWallet, holderWallet2 wallet.Wallet) (offerSequence uint32) { + fmt.Println("⏳ Creating escrow...") + escrow := &transactions.EscrowCreate{ + BaseTx: transactions.BaseTx{ + Account: holderWallet.ClassicAddress, + }, + Amount: txnTypes.IssuedCurrencyAmount{ + Issuer: issuerWallet.ClassicAddress, + Currency: currency.ConvertStringToHex("ABCD"), + Value: "100", + }, + Destination: holderWallet2.ClassicAddress, + CancelAfter: safeInt64ToUint32(rippleTime.UnixTimeToRippleTime(time.Now().Unix()) + 4000), + FinishAfter: safeInt64ToUint32(rippleTime.UnixTimeToRippleTime(time.Now().Unix() + 5)), + } + escrowResponse, err := client.SubmitTxAndWait(escrow.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet, + }) + if err != nil { + fmt.Printf("❌ Error creating escrow: %s\n", err) + return + } + fmt.Println("✅ Escrow created!") + fmt.Printf("🌐 Hash: %s\n", escrowResponse.Hash.String()) + fmt.Printf("🌐 Sequence: %d\n", escrowResponse.TxJSON.Sequence()) + fmt.Println() + + return escrowResponse.TxJSON.Sequence() + +} + +// finishEscrow finishes the escrow for the holder wallet 2. +func finishEscrow(client *websocket.Client, holderWallet, holderWallet2 wallet.Wallet, offerSequence uint32) { + fmt.Println("⏳ Finishing escrow...") + escrow := &transactions.EscrowFinish{ + BaseTx: transactions.BaseTx{ + Account: holderWallet2.ClassicAddress, + }, + Owner: holderWallet.ClassicAddress, + OfferSequence: offerSequence, + } + escrowResponse, err := client.SubmitTxAndWait(escrow.Flatten(), &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &holderWallet2, + }) + if err != nil { + fmt.Printf("❌ Error finishing escrow: %s\n", err) + return + } + fmt.Println("✅ Escrow finished!") + fmt.Printf("🌐 Hash: %s\n", escrowResponse.Hash.String()) + fmt.Println() +} diff --git a/xrpl/ledger-entry-types/account_root.go b/xrpl/ledger-entry-types/account_root.go index c3e6f156..6f4c5af2 100644 --- a/xrpl/ledger-entry-types/account_root.go +++ b/xrpl/ledger-entry-types/account_root.go @@ -8,6 +8,8 @@ import ( const ( // Enable Clawback for this account. (Requires the Clawback amendment.) lsfAllowTrustLineClawback uint32 = 0x80000000 + // Allow IOUs to be used as escrow amounts for an issuer + lsfAllowTrustLineLocking uint32 = 0x40000000 // Enable rippling on this addresses's trust lines by default. Required for issuing addresses; discouraged for others. lsfDefaultRipple uint32 = 0x00800000 // This account has DepositAuth enabled, meaning it can only receive funds from transactions it sends, and from preauthorized accounts. (Added by the DepositAuth amendment) @@ -136,6 +138,11 @@ func (a *AccountRoot) SetLsfAllowTrustLineClawback() { a.Flags |= lsfAllowTrustLineClawback } +// SetLsfAllowTrustLineLocking sets the AllowTrustLineLocking flag. +func (a *AccountRoot) SetLsfAllowTrustLineLocking() { + a.Flags |= lsfAllowTrustLineLocking +} + // SetLsfDefaultRipple sets the DefaultRipple flag. func (a *AccountRoot) SetLsfDefaultRipple() { a.Flags |= lsfDefaultRipple diff --git a/xrpl/ledger-entry-types/account_root_test.go b/xrpl/ledger-entry-types/account_root_test.go index f6c7a749..4d7017d3 100644 --- a/xrpl/ledger-entry-types/account_root_test.go +++ b/xrpl/ledger-entry-types/account_root_test.go @@ -132,3 +132,9 @@ func TestAccountRoot_SetLsfRequireDestTag(t *testing.T) { ar.SetLsfRequireDestTag() require.Equal(t, ar.Flags, lsfRequireDestTag) } + +func TestAccountRoot_SetLsfAllowTrustLineLocking(t *testing.T) { + ar := &AccountRoot{} + ar.SetLsfAllowTrustLineLocking() + require.Equal(t, ar.Flags, lsfAllowTrustLineLocking) +} diff --git a/xrpl/ledger-entry-types/escrow.go b/xrpl/ledger-entry-types/escrow.go index 1863a6bf..36c28718 100644 --- a/xrpl/ledger-entry-types/escrow.go +++ b/xrpl/ledger-entry-types/escrow.go @@ -1,6 +1,8 @@ package ledger import ( + "encoding/json" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" ) @@ -23,7 +25,9 @@ import ( // "PreviousTxnID": "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", // "PreviousTxnLgrSeq": 28991004, // "SourceTag": 11747, -// "index": "DC5F3851D8A1AB622F957761E5963BC5BD439D5C24AC6AD7AC4523F0640244AC" +// "index": "DC5F3851D8A1AB622F957761E5963BC5BD439D5C24AC6AD7AC4523F0640244AC", +// "TransferRate": 1000, +// "IssuerNode": 1234567890 // } // // ``` @@ -39,8 +43,9 @@ type Escrow struct { // The address of the owner (sender) of this escrow. This is the account that provided the XRP, // and gets it back if the escrow is canceled. Account types.Address - // The amount of XRP, in drops, currently held in the escrow. - Amount types.XRPCurrencyAmount + // Amount of XRP or fungible tokens to deduct from the sender's balance and escrow. + // Once escrowed, the payment can either go to the Destination address (after the FinishAfter time) or be returned to the sender (after the CancelAfter time). + Amount types.CurrencyAmount // The escrow can be canceled if and only if this field is present and the time it specifies has passed. // Specifically, this is specified as seconds since the Ripple Epoch and it "has passed" if it's // earlier than the close time of the previous validated ledger. @@ -67,9 +72,64 @@ type Escrow struct { PreviousTxnLgrSeq uint32 // An arbitrary tag to further specify the source for this escrow, such as a hosted recipient at the owner's address. SourceTag uint32 `json:",omitempty"` + // The fee to charge when users finish an escrow, initially set on the creation of an escrow contract and updated on subsequent finish transactions. + TransferRate uint32 `json:",omitempty"` + // (Optional) The ledger index of the issuer's directory node associated with the Escrow. Used when the issuer is neither the source nor destination account. + IssuerNode uint64 `json:",omitempty"` } // EntryType returns the ledger entry type for Escrow. func (*Escrow) EntryType() EntryType { return EscrowEntry } + +// UnmarshalJSON implements custom JSON unmarshalling for Escrow. +func (e *Escrow) UnmarshalJSON(data []byte) error { + type escrowHelper struct { + Index types.Hash256 `json:"index,omitempty"` + LedgerEntryType EntryType + Flags uint32 + Account types.Address + Amount json.RawMessage + CancelAfter uint32 `json:",omitempty"` + Condition string `json:",omitempty"` + Destination types.Address + DestinationNode string `json:",omitempty"` + DestinationTag uint32 `json:",omitempty"` + FinishAfter uint32 `json:",omitempty"` + OwnerNode string + PreviousTxnID types.Hash256 + PreviousTxnLgrSeq uint32 + SourceTag uint32 `json:",omitempty"` + TransferRate uint32 `json:",omitempty"` + IssuerNode uint64 `json:",omitempty"` + } + var h escrowHelper + if err := json.Unmarshal(data, &h); err != nil { + return err + } + *e = Escrow{ + Index: h.Index, + LedgerEntryType: h.LedgerEntryType, + Flags: h.Flags, + Account: h.Account, + CancelAfter: h.CancelAfter, + Condition: h.Condition, + Destination: h.Destination, + DestinationNode: h.DestinationNode, + DestinationTag: h.DestinationTag, + FinishAfter: h.FinishAfter, + OwnerNode: h.OwnerNode, + PreviousTxnID: h.PreviousTxnID, + PreviousTxnLgrSeq: h.PreviousTxnLgrSeq, + SourceTag: h.SourceTag, + TransferRate: h.TransferRate, + IssuerNode: h.IssuerNode, + } + amount, err := types.UnmarshalCurrencyAmount(h.Amount) + if err != nil { + return err + } + e.Amount = amount + return nil +} diff --git a/xrpl/ledger-entry-types/escrow_test.go b/xrpl/ledger-entry-types/escrow_test.go index d161b76e..f69221c9 100644 --- a/xrpl/ledger-entry-types/escrow_test.go +++ b/xrpl/ledger-entry-types/escrow_test.go @@ -24,6 +24,8 @@ func TestEscrow(t *testing.T) { PreviousTxnID: "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", PreviousTxnLgrSeq: 28991004, SourceTag: 11747, + TransferRate: 1000, + IssuerNode: 1234567890, } j := `{ @@ -40,7 +42,9 @@ func TestEscrow(t *testing.T) { "OwnerNode": "0000000000000000", "PreviousTxnID": "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", "PreviousTxnLgrSeq": 28991004, - "SourceTag": 11747 + "SourceTag": 11747, + "TransferRate": 1000, + "IssuerNode": 1234567890 }` if err := testutil.SerializeAndDeserialize(t, s, j); err != nil { @@ -52,3 +56,105 @@ func TestEscrow_EntryType(t *testing.T) { s := &Escrow{} require.Equal(t, s.EntryType(), EscrowEntry) } + +func TestEscrowMPTAmountSerialization(t *testing.T) { + var s Object = &Escrow{ + LedgerEntryType: EscrowEntry, + Flags: 0, + Account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Amount: types.MPTCurrencyAmount{ + MPTIssuanceID: "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", + Value: "10000", + }, + CancelAfter: 545440232, + Condition: "A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120", + Destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + DestinationNode: "0000000000000000", + DestinationTag: 23480, + FinishAfter: 545354132, + OwnerNode: "0000000000000000", + PreviousTxnID: "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", + PreviousTxnLgrSeq: 28991004, + SourceTag: 11747, + TransferRate: 1000, + IssuerNode: 1234567890, + } + + j := `{ + "LedgerEntryType": "Escrow", + "Flags": 0, + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": { + "mpt_issuance_id": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", + "value": "10000" + }, + "CancelAfter": 545440232, + "Condition": "A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120", + "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "DestinationNode": "0000000000000000", + "DestinationTag": 23480, + "FinishAfter": 545354132, + "OwnerNode": "0000000000000000", + "PreviousTxnID": "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", + "PreviousTxnLgrSeq": 28991004, + "SourceTag": 11747, + "TransferRate": 1000, + "IssuerNode": 1234567890 +}` + + if err := testutil.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } +} + +func TestEscrowIssuedAmountSerialization(t *testing.T) { + var s Object = &Escrow{ + LedgerEntryType: EscrowEntry, + Flags: 0, + Account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Amount: types.IssuedCurrencyAmount{ + Issuer: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Currency: "USD", + Value: "10000", + }, + CancelAfter: 545440232, + Condition: "A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120", + Destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + DestinationNode: "0000000000000000", + DestinationTag: 23480, + FinishAfter: 545354132, + OwnerNode: "0000000000000000", + PreviousTxnID: "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", + PreviousTxnLgrSeq: 28991004, + SourceTag: 11747, + TransferRate: 1000, + IssuerNode: 1234567890, + } + + j := `{ + "LedgerEntryType": "Escrow", + "Flags": 0, + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": { + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "currency": "USD", + "value": "10000" + }, + "CancelAfter": 545440232, + "Condition": "A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120", + "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "DestinationNode": "0000000000000000", + "DestinationTag": 23480, + "FinishAfter": 545354132, + "OwnerNode": "0000000000000000", + "PreviousTxnID": "C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7", + "PreviousTxnLgrSeq": 28991004, + "SourceTag": 11747, + "TransferRate": 1000, + "IssuerNode": 1234567890 +}` + + if err := testutil.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } +} diff --git a/xrpl/ledger-entry-types/mptoken_issuance.go b/xrpl/ledger-entry-types/mptoken_issuance.go index e7fc1563..eb8ffe80 100644 --- a/xrpl/ledger-entry-types/mptoken_issuance.go +++ b/xrpl/ledger-entry-types/mptoken_issuance.go @@ -59,6 +59,8 @@ type MPTokenIssuance struct { // The Sequence (or Ticket) number of the transaction that created this issuance. // This helps to uniquely identify the issuance and distinguish it from any other later MPT issuances created by this account. Sequence uint32 + // The amount of tokens currently locked up (for example, in escrow or payment channels). (Requires the TokenEscrow amendment .) + LockedAmount uint64 `json:",omitempty"` } // EntryType returns the type of the ledger entry. diff --git a/xrpl/queries/transactions/tx_request.go b/xrpl/queries/transactions/tx_request.go index 7a00daad..31a51e2d 100644 --- a/xrpl/queries/transactions/tx_request.go +++ b/xrpl/queries/transactions/tx_request.go @@ -50,5 +50,5 @@ type TxResponse struct { // TODO: Improve Meta parsing Meta any `json:"meta"` Validated bool `json:"validated"` - Tx transaction.FlatTransaction `json:",omitempty"` + TxJSON transaction.FlatTransaction `json:"tx_json,omitempty"` } diff --git a/xrpl/transaction/account_set.go b/xrpl/transaction/account_set.go index 4fa8b6a2..68c44300 100644 --- a/xrpl/transaction/account_set.go +++ b/xrpl/transaction/account_set.go @@ -46,6 +46,8 @@ const ( asfDisallowIncomingTrustLine uint32 = 15 // Permanently gain the ability to claw back issued IOUs asfAllowTrustLineClawback uint32 = 16 + // Issuers allow their IOUs to be used as escrow amounts + asfAllowTrustLineLocking uint32 = 17 // // Transaction Flags @@ -334,6 +336,16 @@ func (s *AccountSet) ClearAsfAllowTrustLineClawback() { s.ClearFlag = asfAllowTrustLineClawback } +// SetAsfAllowTrustLineLocking sets the allow trust line locking flag. +func (s *AccountSet) SetAsfAllowTrustLineLocking() { + s.SetFlag = asfAllowTrustLineLocking +} + +// ClearAsfAllowTrustLineLocking clears the allow trust line locking flag. +func (s *AccountSet) ClearAsfAllowTrustLineLocking() { + s.ClearFlag = asfAllowTrustLineLocking +} + // Validate the AccountSet transaction fields. func (s *AccountSet) Validate() (bool, error) { flatten := s.Flatten() @@ -396,7 +408,7 @@ func (s *AccountSet) Validate() (bool, error) { // check if SetFlag is within the valid range if s.SetFlag != 0 { - if s.SetFlag < asfRequireDest || s.SetFlag > asfAllowTrustLineClawback { + if s.SetFlag < asfRequireDest || s.SetFlag > asfAllowTrustLineLocking { return false, ErrAccountSetInvalidSetFlag } } diff --git a/xrpl/transaction/account_set_test.go b/xrpl/transaction/account_set_test.go index 4942cfeb..a2010d7f 100644 --- a/xrpl/transaction/account_set_test.go +++ b/xrpl/transaction/account_set_test.go @@ -204,6 +204,13 @@ func TestAccountSetAsfFlags(t *testing.T) { }, expected: asfAllowTrustLineClawback, }, + { + name: "pass - SetAsfAllowTrustLineLocking", + setter: func(s *AccountSet) { + s.SetAsfAllowTrustLineLocking() + }, + expected: asfAllowTrustLineLocking, + }, } for _, tt := range tests { @@ -328,6 +335,13 @@ func TestAccountClearAsfFlags(t *testing.T) { }, expected: asfAllowTrustLineClawback, }, + { + name: "pass - ClearAsfAllowTrustLineLocking", + setter: func(s *AccountSet) { + s.ClearAsfAllowTrustLineLocking() + }, + expected: asfAllowTrustLineLocking, + }, } for _, tt := range tests { diff --git a/xrpl/transaction/escrow_cancel.go b/xrpl/transaction/escrow_cancel.go index 31d55480..0d086703 100644 --- a/xrpl/transaction/escrow_cancel.go +++ b/xrpl/transaction/escrow_cancel.go @@ -39,7 +39,7 @@ func (e *EscrowCancel) Flatten() FlatTransaction { flattened["TransactionType"] = "EscrowCancel" if e.Owner != "" { - flattened["Owner"] = e.Owner + flattened["Owner"] = e.Owner.String() } if e.OfferSequence != 0 { diff --git a/xrpl/transaction/escrow_create.go b/xrpl/transaction/escrow_create.go index 48b50cb0..622059bb 100644 --- a/xrpl/transaction/escrow_create.go +++ b/xrpl/transaction/escrow_create.go @@ -1,6 +1,8 @@ package transaction import ( + "encoding/json" + addresscodec "github.com/Peersyst/xrpl-go/address-codec" "github.com/Peersyst/xrpl-go/xrpl/transaction/types" ) @@ -26,8 +28,9 @@ import ( // ``` type EscrowCreate struct { BaseTx - // Amount of XRP, in drops, to deduct from the sender's balance and escrow. Once escrowed, the XRP can either go to the Destination address (after the FinishAfter time) or returned to the sender (after the CancelAfter time). - Amount types.XRPCurrencyAmount + // Amount of XRP or fungible tokens to deduct from the sender's balance and escrow. + // Once escrowed, the payment can either go to the Destination address (after the FinishAfter time) or be returned to the sender (after the CancelAfter time). + Amount types.CurrencyAmount // Address to receive escrowed XRP. Destination types.Address // (Optional) The time, in seconds since the Ripple Epoch, when this escrow expires. This value is immutable; the funds can only be returned to the sender after this time. @@ -54,7 +57,7 @@ func (e *EscrowCreate) Flatten() FlatTransaction { flattened["Amount"] = e.Amount.Flatten() if e.Destination != "" { - flattened["Destination"] = e.Destination + flattened["Destination"] = e.Destination.String() } if e.CancelAfter != 0 { flattened["CancelAfter"] = e.CancelAfter @@ -72,6 +75,37 @@ func (e *EscrowCreate) Flatten() FlatTransaction { return flattened } +// UnmarshalJSON implements custom JSON unmarshalling for EscrowCreate. +func (e *EscrowCreate) UnmarshalJSON(data []byte) error { + type escrowCreateHelper struct { + BaseTx + Amount json.RawMessage + Destination types.Address + CancelAfter uint32 `json:",omitempty"` + FinishAfter uint32 `json:",omitempty"` + Condition string `json:",omitempty"` + DestinationTag *uint32 `json:",omitempty"` + } + var h escrowCreateHelper + if err := json.Unmarshal(data, &h); err != nil { + return err + } + *e = EscrowCreate{ + BaseTx: h.BaseTx, + Destination: h.Destination, + CancelAfter: h.CancelAfter, + FinishAfter: h.FinishAfter, + Condition: h.Condition, + DestinationTag: h.DestinationTag, + } + amount, err := types.UnmarshalCurrencyAmount(h.Amount) + if err != nil { + return err + } + e.Amount = amount + return nil +} + // Validate checks the EscrowCreate transaction fields for correctness. func (e *EscrowCreate) Validate() (bool, error) { ok, err := e.BaseTx.Validate() diff --git a/xrpl/transaction/escrow_create_test.go b/xrpl/transaction/escrow_create_test.go index 3f4c61da..dd5e96b2 100644 --- a/xrpl/transaction/escrow_create_test.go +++ b/xrpl/transaction/escrow_create_test.go @@ -348,6 +348,97 @@ func TestEscrowCreate_Unmarshal(t *testing.T) { expectedTag: nil, expectUnmarshalError: false, }, + { + name: "pass - full EscrowCreate with MPTAmount", + jsonData: `{ + "TransactionType": "EscrowCreate", + "Account": "rEXAMPLE123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "Destination": "rDEST123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "Amount": { + "mpt_issuance_id": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", + "value": "1000000" + }, + "Fee": "10", + "Sequence": 1, + "Flags": 2147483648, + "CancelAfter": 695123456, + "FinishAfter": 695000000, + "Condition": "A0258020C4F71E9B01F5A78023E932ABF6B2C1F020986E6C9E55678FFBAE67A2F5B474680103080000000000000000000000000000000000000000000000000000000000000000", + "DestinationTag": 12345, + "SourceTag": 54321, + "OwnerNode": "0000000000000000", + "PreviousTxnID": "C4F71E9B01F5A78023E932ABF6B2C1F020986E6C9E55678FFBAE67A2F5B47468", + "LastLedgerSequence": 12345678, + "NetworkID": 1024, + "Memos": [ + { + "Memo": { + "MemoType": "657363726F77", + "MemoData": "457363726F77206372656174656420666F72207061796D656E74" + } + } + ], + "Signers": [ + { + "Signer": { + "Account": "rSIGNER123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "SigningPubKey": "ED5F93AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12345678", + "TxnSignature": "3045022100D7F67A81F343...B87D" + } + } + ], + "SigningPubKey": "ED5F93AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12345678", + "TxnSignature": "3045022100D7F67A81F343...B87D" + }`, + expectedTag: func() *uint32 { v := uint32(12345); return &v }(), + expectUnmarshalError: false, + }, + { + name: "pass - full EscrowCreate with IssuedAmount", + jsonData: `{ + "TransactionType": "EscrowCreate", + "Account": "rEXAMPLE123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "Destination": "rDEST123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "Amount": { + "issuer": "rEXAMPLE123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "currency": "USD", + "value": "1000000" + }, + "Fee": "10", + "Sequence": 1, + "Flags": 2147483648, + "CancelAfter": 695123456, + "FinishAfter": 695000000, + "Condition": "A0258020C4F71E9B01F5A78023E932ABF6B2C1F020986E6C9E55678FFBAE67A2F5B474680103080000000000000000000000000000000000000000000000000000000000000000", + "DestinationTag": 12345, + "SourceTag": 54321, + "OwnerNode": "0000000000000000", + "PreviousTxnID": "C4F71E9B01F5A78023E932ABF6B2C1F020986E6C9E55678FFBAE67A2F5B47468", + "LastLedgerSequence": 12345678, + "NetworkID": 1024, + "Memos": [ + { + "Memo": { + "MemoType": "657363726F77", + "MemoData": "457363726F77206372656174656420666F72207061796D656E74" + } + } + ], + "Signers": [ + { + "Signer": { + "Account": "rSIGNER123456789ABCDEFGHJKLMNPQRSTUVWXYZ", + "SigningPubKey": "ED5F93AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12345678", + "TxnSignature": "3045022100D7F67A81F343...B87D" + } + } + ], + "SigningPubKey": "ED5F93AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12345678", + "TxnSignature": "3045022100D7F67A81F343...B87D" + }`, + expectedTag: func() *uint32 { v := uint32(12345); return &v }(), + expectUnmarshalError: false, + }, } for _, tt := range tests { diff --git a/xrpl/transaction/escrow_finish.go b/xrpl/transaction/escrow_finish.go index 8045422a..d174ba42 100644 --- a/xrpl/transaction/escrow_finish.go +++ b/xrpl/transaction/escrow_finish.go @@ -49,7 +49,7 @@ func (e *EscrowFinish) Flatten() FlatTransaction { flattened["TransactionType"] = "EscrowFinish" if e.Owner != "" { - flattened["Owner"] = e.Owner + flattened["Owner"] = e.Owner.String() } if e.OfferSequence != 0 { diff --git a/xrpl/transaction/flat_tx.go b/xrpl/transaction/flat_tx.go index b93181a2..e40d64b2 100644 --- a/xrpl/transaction/flat_tx.go +++ b/xrpl/transaction/flat_tx.go @@ -1,5 +1,9 @@ package transaction +import ( + "encoding/json" +) + var _ Tx = (*FlatTransaction)(nil) // FlatTransaction is a flattened transaction represented as a map from field names to interface{} values. @@ -14,3 +18,34 @@ func (f FlatTransaction) TxType() TxType { } return TxType(txType) } + +// Sequence returns the sequence number of the flattened transaction. +func (f FlatTransaction) Sequence() uint32 { + sequence, ok := f["Sequence"].(json.Number) + if ok { + sequenceInt, err := sequence.Float64() + if err != nil { + return 0 + } + return uint32(sequenceInt) + } + + // Handle float64 case (when JSON is parsed as float64 instead of json.Number) + if sequenceFloat, ok := f["Sequence"].(float64); ok { + return uint32(sequenceFloat) + } + + // Handle uint32 case (direct integer) + if sequenceInt, ok := f["Sequence"].(uint32); ok { + return sequenceInt + } + + // Handle int case + if sequenceInt, ok := f["Sequence"].(int); ok { + if sequenceInt >= 0 && sequenceInt <= int(^uint32(0)) { + return uint32(sequenceInt) + } + } + + return 0 +} diff --git a/xrpl/transaction/payment_channel_claim.go b/xrpl/transaction/payment_channel_claim.go index 0976fcd4..63713925 100644 --- a/xrpl/transaction/payment_channel_claim.go +++ b/xrpl/transaction/payment_channel_claim.go @@ -68,7 +68,7 @@ type PaymentChannelClaim struct { // Must be more than the total amount delivered by the channel so far, but not greater than the Amount of the signed claim. Must be provided except when closing the channel. Balance types.XRPCurrencyAmount `json:",omitempty"` // (Optional) The amount of XRP, in drops, authorized by the Signature. This must match the amount in the signed message. - // This is the cumulative amount of XRP that can be dispensed by the channel, including XRP previously redeemed. + // This is the cumulative amount of XRP that can be dispensed by the channel, including XRP previously redeemed. Must be provided except when closing the channel. Amount types.XRPCurrencyAmount `json:",omitempty"` // (Optional) The signature of this claim, as hexadecimal. The signed message contains the channel ID and the amount of the claim. // Required unless the sender of the transaction is the source address of the channel. diff --git a/xrpl/transaction/types/domain.go b/xrpl/transaction/types/domain.go index 523a505e..ddd44dc4 100644 --- a/xrpl/transaction/types/domain.go +++ b/xrpl/transaction/types/domain.go @@ -1,3 +1,4 @@ +//revive:disable:var-naming package types // Domain returns the domain that owns this account, as a string of hex representing the.