Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Adds `AMMClawback` transaction type.
- Adds `MPTokenAuthorize`, `MPTokenIssuanceCreate`, `MPTokenIssuanceDestroy`, `MPTokenIssuanceSet` transactions. It also adds the `types.Holder`, `types.AssetScale`, `types.MPTokenMetadata` and `types.TransferFee` types to represent the holder of the token, the asset scale, the metadata and the transfer fee of the token respectively.
- Adds `NFTokenMintOffer` support by adding `Amount`, `Expiration`, and `Destination` fields to `NFTokenMint` transaction. Also add `NFTokenMintMetadata` struct to handle transaction metadata with `nftoken_id` and `offer_id` fields.
- Adds `MPTCurrencyAmount` for currency kinds.
- Adds unit tests for `MPTCurrencyAmount`.
- Adds `NFTokenModify` transaction type.
Expand Down
30 changes: 17 additions & 13 deletions xrpl/transaction/nftoken_mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (
)

var (
ErrExpirationRequiresAmount = errors.New("the Amount field is required when Expiration is set")
// ErrInvalidTransferFee is returned when the transferFee is not between 0 and 50000 inclusive.
ErrInvalidTransferFee = errors.New("transferFee must be between 0 and 50000 inclusive")
// ErrIssuerAccountConflict is returned when the issuer is the same as the account.
ErrIssuerAccountConflict = errors.New("issuer cannot be the same as the account")
// ErrTransferFeeRequiresTransferableFlag is returned when the transferFee is set without the tfTransferable flag.
ErrTransferFeeRequiresTransferableFlag = errors.New("transferFee can only be set if the tfTransferable flag is enabled")
// ErrAmountRequiredWithExpirationOrDestination is returned when Expiration or Destination is set without Amount.
ErrAmountRequiredWithExpirationOrDestination = errors.New("amount is required when Expiration or Destination is present")
)

// The NFTokenMint transaction creates a non-fungible token and adds it to the relevant NFTokenPage object of the NFTokenMinter as an NFToken object.
Expand Down Expand Up @@ -56,7 +57,7 @@ type NFTokenMint struct {
// (Optional) The value specifies the fee charged by the issuer for secondary sales of the NFToken, if such sales are allowed.
// Valid values for this field are between 0 and 50000 inclusive, allowing transfer rates of between 0.00% and 50.00% in increments of 0.001.
// If this field is provided, the transaction MUST have the tfTransferable flag enabled.
TransferFee uint16 `json:",omitempty"`
TransferFee *uint16 `json:",omitempty"`
// (Optional) Up to 256 bytes of arbitrary data. In JSON, this should be encoded as a string of hexadecimal.
// You can use the xrpl.convertStringToHex utility to convert a URI to its hexadecimal equivalent.
// This is intended to be a URI that points to the data or metadata associated with the NFT.
Expand All @@ -70,7 +71,7 @@ type NFTokenMint struct {
Amount types.CurrencyAmount `json:",omitempty"`
// (Optional) Time after which the offer is no longer active, in seconds since the Ripple Epoch.
// Results in an error if the Amount field is not specified.
Expiration uint32 `json:",omitempty"`
Expiration *uint32 `json:",omitempty"`
// (Optional) If present, indicates that this offer may only be accepted by the specified account.
// Attempts by other accounts to accept this offer MUST fail. Results in an error if the Amount field is not specified.
Destination types.Address `json:",omitempty"`
Expand Down Expand Up @@ -134,8 +135,8 @@ func (n *NFTokenMint) Flatten() FlatTransaction {
flattened["Issuer"] = n.Issuer.String()
}

if n.TransferFee != 0 {
flattened["TransferFee"] = n.TransferFee
if n.TransferFee != nil {
flattened["TransferFee"] = *n.TransferFee
}

if n.URI != "" {
Expand All @@ -146,8 +147,8 @@ func (n *NFTokenMint) Flatten() FlatTransaction {
flattened["Amount"] = n.Amount.Flatten()
}

if n.Expiration != 0 {
flattened["Expiration"] = n.Expiration
if n.Expiration != nil {
flattened["Expiration"] = *n.Expiration
}

if n.Destination != "" {
Expand All @@ -170,7 +171,7 @@ func (n *NFTokenMint) Validate() (bool, error) {
}

// check transfer fee is between 0 and 50000
if n.TransferFee > MaxTransferFee {
if n.TransferFee != nil && *n.TransferFee > MaxTransferFee {
return false, ErrInvalidTransferFee
}

Expand All @@ -190,10 +191,17 @@ func (n *NFTokenMint) Validate() (bool, error) {
}

// check transfer fee can only be set if the tfTransferable flag is enabled
if n.TransferFee > 0 && !IsFlagEnabled(n.Flags, tfTransferable) {
if n.TransferFee != nil && *n.TransferFee > 0 && !IsFlagEnabled(n.Flags, tfTransferable) {
return false, ErrTransferFeeRequiresTransferableFlag
}

// check Amount is required when Expiration or Destination is present
if n.Amount == nil {
if n.Expiration != nil || n.Destination != "" {
return false, ErrAmountRequiredWithExpirationOrDestination
}
}

if ok, err := IsAmount(n.Amount, "Amount", false); !ok {
return false, err
}
Expand All @@ -202,9 +210,5 @@ func (n *NFTokenMint) Validate() (bool, error) {
return false, ErrInvalidDestination
}

if n.Expiration != 0 && n.Amount == nil {
return false, ErrExpirationRequiresAmount
}

return true, nil
}
15 changes: 15 additions & 0 deletions xrpl/transaction/nftoken_mint_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package transaction

import (
"github.com/Peersyst/xrpl-go/xrpl/transaction/types"
)

type NFTokenMintMetadata struct {
TxObjMeta
// rippled 1.11.0 or later
NFTokenID *types.NFTokenID `json:"nftoken_id,omitempty"`
// if Amount is present
OfferID *types.Hash256 `json:"offer_id,omitempty"`
}

func (NFTokenMintMetadata) TxMeta() {}
127 changes: 127 additions & 0 deletions xrpl/transaction/nftoken_mint_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package transaction

import (
"encoding/json"
"testing"

"github.com/Peersyst/xrpl-go/xrpl/transaction/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNFTokenMintMetadata_JSONMarshal(t *testing.T) {
tests := []struct {
name string
metadata *NFTokenMintMetadata
expected string
}{
{
name: "pass - with both NFTokenID and OfferID",
metadata: &NFTokenMintMetadata{
NFTokenID: func() *types.NFTokenID {
id := types.NFTokenID("000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65")
return &id
}(),
OfferID: func() *types.Hash256 {
hash := types.Hash256("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77")
return &hash
}(),
},
expected: `{"nftoken_id":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","offer_id":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"}`,
},
{
name: "pass - empty metadata",
metadata: &NFTokenMintMetadata{},
expected: `{}`,
},
{
name: "pass - with full metadata including TxObjMeta",
metadata: &NFTokenMintMetadata{
TxObjMeta: TxObjMeta{
TransactionIndex: 123,
TransactionResult: "tesSUCCESS",
},
NFTokenID: func() *types.NFTokenID {
id := types.NFTokenID("000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65")
return &id
}(),
OfferID: func() *types.Hash256 {
hash := types.Hash256("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77")
return &hash
}(),
},
expected: `{"TransactionIndex":123,"TransactionResult":"tesSUCCESS","nftoken_id":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","offer_id":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"}`,
},
{
name: "pass - nil pointers should omit fields",
metadata: &NFTokenMintMetadata{
NFTokenID: nil,
OfferID: nil,
},
expected: `{}`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
jsonBytes, err := json.Marshal(tt.metadata)
require.NoError(t, err)
assert.JSONEq(t, tt.expected, string(jsonBytes))
})
}
}

func TestNFTokenMintMetadata_JSONUnmarshal(t *testing.T) {
tests := []struct {
name string
json string
expected *NFTokenMintMetadata
}{
{
name: "pass - with both NFTokenID and OfferID",
json: `{"nftoken_id":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","offer_id":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"}`,
expected: &NFTokenMintMetadata{
NFTokenID: func() *types.NFTokenID {
id := types.NFTokenID("000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65")
return &id
}(),
OfferID: func() *types.Hash256 {
hash := types.Hash256("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77")
return &hash
}(),
},
},
{
name: "pass - empty metadata",
json: `{}`,
expected: &NFTokenMintMetadata{},
},
{
name: "pass - with full metadata including TxObjMeta fields",
json: `{"TransactionIndex":123,"TransactionResult":"tesSUCCESS","nftoken_id":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","offer_id":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"}`,
expected: &NFTokenMintMetadata{
TxObjMeta: TxObjMeta{
TransactionIndex: 123,
TransactionResult: "tesSUCCESS",
},
NFTokenID: func() *types.NFTokenID {
id := types.NFTokenID("000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65")
return &id
}(),
OfferID: func() *types.Hash256 {
hash := types.Hash256("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77")
return &hash
}(),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var metadata NFTokenMintMetadata
err := json.Unmarshal([]byte(tt.json), &metadata)
require.NoError(t, err)
assert.Equal(t, tt.expected, &metadata)
})
}
}
Loading
Loading