feat: NFT support (#1829)

* add NFTokenBurn and NFTokenMint

* add NFTokenCreateOffer

* add NFTokenCancelOffer

* add NFTokenAcceptOffer

* add requests and responses

* make a beta 2.1.0

* rename TokenIDs to TokenOffers

* add validations and fixup docs

* add tests

* add missing error codes to binary codec

* adds history changes
This commit is contained in:
ledhed2222
2021-12-17 18:26:47 -05:00
committed by GitHub
parent c0a19a8417
commit 46a8adcac9
26 changed files with 1314 additions and 41 deletions

2
package-lock.json generated
View File

@@ -2803,7 +2803,6 @@
},
"node_modules/@types/create-hash": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.2.tgz",
"integrity": "sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ==",
"dev": true,
"dependencies": {
@@ -18861,7 +18860,6 @@
},
"@types/create-hash": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.2.tgz",
"integrity": "sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ==",
"dev": true,
"requires": {

View File

@@ -1,5 +1,11 @@
# ripple-binary-codec Release History
## Unreleased
### Added
- Exported `TRANSACTION_TYPES` value
### Fixed
- Adds missing fields from XLS-20 NFT implementation
## 1.2.2 (2021-12-2)
- Fix issue where unsupported currency codes weren't being correctly processed
- Added a workaround for rippled UNLModify encoding bug (#1830)

View File

@@ -855,6 +855,16 @@
"type": "Amount"
}
],
[
"BrokerFee",
{
"nth": 19,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Amount"
}
],
[
"taker_gets_funded",
{
@@ -1536,7 +1546,7 @@
}
],
[
"TokenIDs",
"TokenOffers",
{
"nth": 4,
"isVLEncoded": true,
@@ -1856,6 +1866,8 @@
"tefBAD_AUTH_MASTER": -183,
"tefINVARIANT_FAILED": -182,
"tefTOO_BIG": -181,
"tefNO_TICKET": -180,
"tefTOKEN_IS_NOT_TRANSFERABLE": -179,
"terRETRY": -99,
"terFUNDS_SPENT": -98,

View File

@@ -3,6 +3,13 @@ import { SerializedType } from '../types/serialized-type'
import { Buffer } from 'buffer/'
import { BytesList } from '../binary'
/*
* @brief: All valid transaction types
*/
export const TRANSACTION_TYPES = Object.entries(enums.TRANSACTION_TYPES)
.filter(([_key, value]) => value >= 0)
.map(([key, _value]) => key)
const TYPE_WIDTH = 2
const LEDGER_ENTRY_WIDTH = 2
const TRANSACTION_TYPE_WIDTH = 2

View File

@@ -3,6 +3,8 @@ import { quality, binary } from './coretypes'
import { decodeLedgerData } from './ledger-hashes'
import { ClaimObject } from './binary'
import { JsonObject } from './types/serialized-type'
import { TRANSACTION_TYPES } from './enums'
const {
signingData,
signingClaimData,
@@ -109,4 +111,5 @@ export = {
encodeQuality,
decodeQuality,
decodeLedgerData,
TRANSACTION_TYPES,
}

View File

@@ -328,7 +328,7 @@
"LastLedgerSequence": 27,
"Sequence": 10,
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TokenIDs": [
"TokenOffers": [
"00090032B5F762798A53D543A014CAF8B297CFF8F2F937E80000099B00000000"
],
"TransactionType": "NFTokenCancelOffer",

View File

@@ -2,6 +2,10 @@
Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release.
## Unreleased
### Added
* Support for the [XLS-20 NFT proposal](https://github.com/XRPLF/XRPL-Standards/discussions/46)
## 2.0.3 (2021-12-1)
* Removes requirement for npm version > 7 for non-contributors
* For contributors -

View File

@@ -65,3 +65,18 @@ export interface SignerEntry {
SignerWeight: number
}
}
/**
* One offer that might be returned from either an {@link NFTBuyOffersRequest}
* or an {@link NFTSellOffersRequest}.
*
* @category Responses
*/
export interface NFTOffer {
amount: Amount
flags: number
index: string
owner: string
destination?: string
expiration?: number
}

View File

@@ -0,0 +1,71 @@
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `account_nfts` method retrieves all of the NFTs currently owned by the
* specified account.
*
* @category Requests
*/
export interface AccountNFTsRequest extends BaseRequest {
command: 'account_nfts'
/**
* The unique identifier of an account, typically the account's address. The
* request returns NFTs owned by this account.
*/
account: string
/**
* Limit the number of NFTokens to retrieve.
*/
limit?: number
/**
* Value from a previous paginated response. Resume retrieving data where
* that response left off.
*/
marker?: unknown
}
/**
* One NFToken that might be returned from an {@link AccountNFTsRequest}.
*
* @category Responses
*/
interface AccountNFToken {
Flags: number
Issuer: string
TokenID: string
TokenTaxon: number
nft_serial: number
}
/**
* Response expected from an {@link AccountNFTsRequest}.
*
* @category Responses
*/
export interface AccountNFTsResponse extends BaseResponse {
result: {
/**
* The account requested.
*/
account: string
/**
* A list of NFTs owned by the specified account.
*/
account_nfts: AccountNFToken[]
/**
* The ledger index of the current open ledger, which was used when
* retrieving this information.
*/
ledger_current_index: number
/** If true, this data comes from a validated ledger. */
validated: boolean
/**
* Server-defined value indicating the response is paginated. Pass this to
* the next call to resume where this call left off. Omitted when there are
* No additional pages after this one.
*/
marker?: unknown
/** The limit that was used to fulfill this request. */
limit?: number
}
}

View File

@@ -8,6 +8,7 @@ import {
} from './accountCurrencies'
import { AccountInfoRequest, AccountInfoResponse } from './accountInfo'
import { AccountLinesRequest, AccountLinesResponse } from './accountLines'
import { AccountNFTsRequest, AccountNFTsResponse } from './accountNFTs'
import { AccountObjectsRequest, AccountObjectsResponse } from './accountObjects'
import { AccountOffersRequest, AccountOffersResponse } from './accountOffers'
import { AccountTxRequest, AccountTxResponse } from './accountTx'
@@ -29,6 +30,8 @@ import { LedgerCurrentRequest, LedgerCurrentResponse } from './ledgerCurrent'
import { LedgerDataRequest, LedgerDataResponse } from './ledgerData'
import { LedgerEntryRequest, LedgerEntryResponse } from './ledgerEntry'
import { ManifestRequest, ManifestResponse } from './manifest'
import { NFTBuyOffersRequest, NFTBuyOffersResponse } from './nftBuyOffers'
import { NFTSellOffersRequest, NFTSellOffersResponse } from './nftSellOffers'
import { NoRippleCheckRequest, NoRippleCheckResponse } from './norippleCheck'
import {
PathFindRequest,
@@ -70,10 +73,12 @@ import { UnsubscribeRequest, UnsubscribeResponse } from './unsubscribe'
* @category Requests
*/
type Request =
// account methods
| AccountChannelsRequest
| AccountCurrenciesRequest
| AccountInfoRequest
| AccountLinesRequest
| AccountNFTsRequest
| AccountObjectsRequest
| AccountOffersRequest
| AccountTxRequest
@@ -108,15 +113,20 @@ type Request =
// utility methods
| PingRequest
| RandomRequest
// NFT methods
| NFTBuyOffersRequest
| NFTSellOffersRequest
/**
* @category Responses
*/
type Response =
// account methods
| AccountChannelsResponse
| AccountCurrenciesResponse
| AccountInfoResponse
| AccountLinesResponse
| AccountNFTsResponse
| AccountObjectsResponse
| AccountOffersResponse
| AccountTxResponse
@@ -151,6 +161,9 @@ type Response =
// utility methods
| PingResponse
| RandomResponse
// NFT methods
| NFTBuyOffersResponse
| NFTSellOffersResponse
export {
Request,
@@ -164,6 +177,8 @@ export {
AccountInfoResponse,
AccountLinesRequest,
AccountLinesResponse,
AccountNFTsRequest,
AccountNFTsResponse,
AccountObjectsRequest,
AccountObjectsResponse,
AccountOffersRequest,
@@ -238,4 +253,9 @@ export {
RandomRequest,
RandomResponse,
ErrorResponse,
// NFT methods
NFTBuyOffersRequest,
NFTBuyOffersResponse,
NFTSellOffersRequest,
NFTSellOffersResponse,
}

View File

@@ -0,0 +1,35 @@
import { NFTOffer } from '../common'
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `nft_buy_offers` method retrieves all of buy offers for the specified
* NFToken.
*
* @category Requests
*/
export interface NFTBuyOffersRequest extends BaseRequest {
command: 'nft_buy_offers'
/**
* The unique identifier of an NFToken. The request returns buy offers for this NFToken.
*/
tokenid: string
}
/**
* Response expected from an {@link NFTBuyOffersRequest}.
*
* @category Responses
*/
export interface NFTBuyOffersResponse extends BaseResponse {
result: {
/**
* A list of buy offers for the specified NFToken.
*/
offers: NFTOffer[]
/**
* The token ID of the NFToken to which these offers pertain.
*/
tokenid: string
}
}

View File

@@ -0,0 +1,35 @@
import { NFTOffer } from '../common'
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `nft_sell_offers` method retrieves all of sell offers for the specified
* NFToken.
*
* @category Requests
*/
export interface NFTSellOffersRequest extends BaseRequest {
command: 'nft_sell_offers'
/**
* The unique identifier of an NFToken. The request returns sell offers for this NFToken.
*/
tokenid: string
}
/**
* Response expected from an {@link NFTSellOffersRequest}.
*
* @category Responses
*/
export interface NFTSellOffersResponse extends BaseResponse {
result: {
/**
* A list of sell offers for the specified NFToken.
*/
offers: NFTOffer[]
/**
* The token ID of the NFToken to which these offers pertain.
*/
tokenid: string
}
}

View File

@@ -0,0 +1,104 @@
import { ValidationError } from '../../errors'
import { Amount } from '../common'
import {
BaseTransaction,
parseAmountValue,
validateBaseTransaction,
} from './common'
/**
* The NFTokenOfferAccept transaction is used to accept offers
* to buy or sell an NFToken. It can either:
*
* 1. Allow one offer to be accepted. This is called direct
* mode.
* 2. Allow two distinct offers, one offering to buy a
* given NFToken and the other offering to sell the same
* NFToken, to be accepted in an atomic fashion. This is
* called brokered mode.
*
* To indicate direct mode, use either the `sell_offer` or
* `buy_offer` fields, but not both. To indicate brokered mode,
* use both the `sell_offer` and `buy_offer` fields. If you use
* neither `sell_offer` nor `buy_offer`, the transaction is invalid.
*/
export interface NFTokenAcceptOffer extends BaseTransaction {
TransactionType: 'NFTokenAcceptOffer'
/**
* Identifies the NFTokenOffer that offers to sell the NFToken.
*
* In direct mode this field is optional, but either SellOffer or
* BuyOffer must be specified. In brokered mode, both SellOffer
* and BuyOffer must be specified.
*/
SellOffer?: string
/**
* Identifies the NFTokenOffer that offers to buy the NFToken.
*
* In direct mode this field is optional, but either SellOffer or
* BuyOffer must be specified. In brokered mode, both SellOffer
* and BuyOffer must be specified.
*/
BuyOffer?: string
/**
* This field is only valid in brokered mode. It specifies the
* amount that the broker will keep as part of their fee for
* bringing the two offers together; the remaining amount will
* be sent to the seller of the NFToken being bought. If
* specified, the fee must be such that, prior to accounting
* for the transfer fee charged by the issuer, the amount that
* the seller would receive is at least as much as the amount
* indicated in the sell offer.
*
* This functionality is intended to allow the owner of an
* NFToken to offer their token for sale to a third party
* broker, who may then attempt to sell the NFToken on for a
* larger amount, without the broker having to own the NFToken
* or custody funds.
*
* Note: in brokered mode, the offers referenced by BuyOffer
* and SellOffer must both specify the same TokenID; that is,
* both must be for the same NFToken.
*/
BrokerFee?: Amount
}
function validateBrokerFee(tx: Record<string, unknown>): void {
const value = parseAmountValue(tx.BrokerFee)
if (Number.isNaN(value)) {
throw new ValidationError('NFTokenAcceptOffer: invalid BrokerFee')
}
if (value <= 0) {
throw new ValidationError(
'NFTokenAcceptOffer: BrokerFee must be greater than 0; omit if there is no fee',
)
}
if (tx.SellOffer == null || tx.BuyOffer == null) {
throw new ValidationError(
'NFTokenAcceptOffer: both SellOffer and BuyOffer must be set if using brokered mode',
)
}
}
/**
* Verify the form and type of an NFTokenAcceptOffer at runtime.
*
* @param tx - An NFTokenAcceptOffer Transaction.
* @throws When the NFTokenAcceptOffer is Malformed.
*/
export function validateNFTokenAcceptOffer(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.BrokerFee != null) {
validateBrokerFee(tx)
}
if (tx.SellOffer == null && tx.BuyOffer == null) {
throw new ValidationError(
'NFTokenAcceptOffer: must set either SellOffer or BuyOffer',
)
}
}

View File

@@ -0,0 +1,42 @@
import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common'
/**
* The NFTokenBurn transaction is used to remove an NFToken object from the
* NFTokenPage in which it is being held, effectively removing the token from
* the ledger ("burning" it).
*
* If this operation succeeds, the corresponding NFToken is removed. If this
* operation empties the NFTokenPage holding the NFToken or results in the
* consolidation, thus removing an NFTokenPage, the owners reserve requirement
* is reduced by one.
*/
export interface NFTokenBurn extends BaseTransaction {
TransactionType: 'NFTokenBurn'
/**
* Indicates the AccountID that submitted this transaction. The account MUST
* be either the present owner of the token or, if the lsfBurnable flag is set
* in the NFToken, either the issuer account or an account authorized by the
* issuer, i.e. MintAccount.
*/
Account: string
/**
* Identifies the NFToken object to be removed by the transaction.
*/
TokenID: string
}
/**
* Verify the form and type of an NFTokenBurn at runtime.
*
* @param tx - An NFTokenBurn Transaction.
* @throws When the NFTokenBurn is Malformed.
*/
export function validateNFTokenBurn(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.TokenID == null) {
throw new ValidationError('NFTokenBurn: missing field TokenID')
}
}

View File

@@ -0,0 +1,45 @@
import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common'
/**
* The NFTokenCancelOffer transaction deletes existing NFTokenOffer objects.
* It is useful if you want to free up space on your account to lower your
* reserve requirement.
*
* The transaction can be executed by the account that originally created
* the NFTokenOffer, the account in the `Recipient` field of the NFTokenOffer
* (if present), or any account if the NFTokenOffer has an `Expiration` and
* the NFTokenOffer has already expired.
*/
export interface NFTokenCancelOffer extends BaseTransaction {
TransactionType: 'NFTokenCancelOffer'
/**
* An array of identifiers of NFTokenOffer objects that should be cancelled
* by this transaction.
*
* It is an error if an entry in this list points to an
* object that is not an NFTokenOffer object. It is not an
* error if an entry in this list points to an object that
* does not exist. This field is required.
*/
TokenOffers: string[]
}
/**
* Verify the form and type of an NFTokenCancelOffer at runtime.
*
* @param tx - An NFTokenCancelOffer Transaction.
* @throws When the NFTokenCancelOffer is Malformed.
*/
export function validateNFTokenCancelOffer(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (!Array.isArray(tx.TokenOffers)) {
throw new ValidationError('NFTokenCancelOffer: missing field TokenOffers')
}
if (tx.TokenOffers.length < 1) {
throw new ValidationError('NFTokenCancelOffer: empty field TokenOffers')
}
}

View File

@@ -0,0 +1,145 @@
import { ValidationError } from '../../errors'
import { Amount } from '../common'
import { isFlagEnabled } from '../utils'
import {
BaseTransaction,
GlobalFlags,
validateBaseTransaction,
isAmount,
parseAmountValue,
} from './common'
/**
* Transaction Flags for an NFTokenCreateOffer Transaction.
*
* @category Transaction Flags
*/
export enum NFTokenCreateOfferFlags {
/**
* If set, indicates that the offer is a sell offer.
* Otherwise, it is a buy offer.
*/
tfSellToken = 0x00000001,
}
/**
* Map of flags to boolean values representing {@link NFTokenCreateOffer} transaction
* flags.
*
* @category Transaction Flags
*/
export interface NFTokenCreateOfferFlagsInterface extends GlobalFlags {
tfSellToken?: boolean
}
/**
* The NFTokenCreateOffer transaction creates either an offer to buy an
* NFT the submitting account does not own, or an offer to sell an NFT
* the submitting account does own.
*/
export interface NFTokenCreateOffer extends BaseTransaction {
TransactionType: 'NFTokenCreateOffer'
/**
* Identifies the TokenID of the NFToken object that the
* offer references.
*/
TokenID: string
/**
* Indicates the amount expected or offered for the Token.
*
* The amount must be non-zero, except when this is a sell
* offer and the asset is XRP. This would indicate that the current
* owner of the token is giving it away free, either to anyone at all,
* or to the account identified by the Destination field.
*/
Amount: Amount
/**
* Indicates the AccountID of the account that owns the
* corresponding NFToken.
*
* If the offer is to buy a token, this field must be present
* and it must be different than Account (since an offer to
* buy a token one already holds is meaningless).
*
* If the offer is to sell a token, this field must not be
* present, as the owner is, implicitly, the same as Account
* (since an offer to sell a token one doesn't already hold
* is meaningless).
*/
Owner?: string
/**
* Indicates the time after which the offer will no longer
* be valid. The value is the number of seconds since the
* Ripple Epoch.
*/
Expiration?: number
/**
* If present, indicates that this offer may only be
* accepted by the specified account. Attempts by other
* accounts to accept this offer MUST fail.
*/
Destination?: string
Flags?: number | NFTokenCreateOfferFlagsInterface
}
function validateSellOfferCases(tx: Record<string, unknown>): void {
if (tx.Owner != null) {
throw new ValidationError(
'NFTokenCreateOffer: Owner must not be present for sell offers',
)
}
}
function validateBuyOfferCases(tx: Record<string, unknown>): void {
if (tx.Owner == null) {
throw new ValidationError(
'NFTokenCreateOffer: Owner must be present for buy offers',
)
}
if (parseAmountValue(tx.Amount) <= 0) {
throw new ValidationError(
'NFTokenCreateOffer: Amount must be greater than 0 for buy offers',
)
}
}
/**
* Verify the form and type of an NFTokenCreateOffer at runtime.
*
* @param tx - An NFTokenCreateOffer Transaction.
* @throws When the NFTokenCreateOffer is Malformed.
*/
export function validateNFTokenCreateOffer(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.Account === tx.Owner) {
throw new ValidationError(
'NFTokenCreateOffer: Owner and Account must not be equal',
)
}
if (tx.Account === tx.Destination) {
throw new ValidationError(
'NFTokenCreateOffer: Destination and Account must not be equal',
)
}
if (tx.TokenID == null) {
throw new ValidationError('NFTokenCreateOffer: missing field TokenID')
}
if (!isAmount(tx.Amount)) {
throw new ValidationError('NFTokenCreateOffer: invalid Amount')
}
if (
typeof tx.Flags === 'number' &&
isFlagEnabled(tx.Flags, NFTokenCreateOfferFlags.tfSellToken)
) {
validateSellOfferCases(tx)
} else {
validateBuyOfferCases(tx)
}
}

View File

@@ -0,0 +1,111 @@
import { ValidationError } from '../../errors'
import { BaseTransaction, GlobalFlags, validateBaseTransaction } from './common'
/**
* Transaction Flags for an NFTokenMint Transaction.
*
* @category Transaction Flags
*/
export enum NFTokenMintFlags {
/**
* If set, indicates that the minted token may be burned by the issuer even
* if the issuer does not currently hold the token. The current holder of
* the token may always burn it.
*/
tfBurnable = 0x00000001,
/**
* If set, indicates that the token may only be offered or sold for XRP.
*/
tfOnlyXRP = 0x00000002,
/**
* If set, indicates that the issuer wants a trustline to be automatically
* created.
*/
tfTrustLine = 0x00000004,
/**
* If set, indicates that this NFT can be transferred. This flag has no
* effect if the token is being transferred from the issuer or to the
* issuer.
*/
tfTransferable = 0x00000008,
}
/**
* Map of flags to boolean values representing {@link NFTokenMint} transaction
* flags.
*
* @category Transaction Flags
*/
export interface NFTokenMintFlagsInterface extends GlobalFlags {
tfBurnable?: boolean
tfOnlyXRP?: boolean
tfTrustLine?: boolean
tfTransferable?: boolean
}
/**
* The NFTokenMint transaction creates an NFToken object and adds it to the
* relevant NFTokenPage object of the minter. If the transaction is
* successful, the newly minted token will be owned by the minter account
* specified by the transaction.
*/
export interface NFTokenMint extends BaseTransaction {
TransactionType: 'NFTokenMint'
/**
* Indicates the taxon associated with this token. The taxon is generally a
* value chosen by the minter of the token and a given taxon may be used for
* multiple tokens. The implementation reserves taxon identifiers greater
* than or equal to 2147483648 (0x80000000). If you have no use for this
* field, set it to 0.
*/
TokenTaxon: number
/**
* Indicates the account that should be the issuer of this token. This value
* is optional and should only be specified if the account executing the
* transaction is not the `Issuer` of the `NFToken` object. If it is
* present, the `MintAccount` field in the `AccountRoot` of the `Issuer`
* field must match the `Account`, otherwise the transaction will fail.
*/
Issuer?: string
/**
* Specifies the fee charged by the issuer for secondary sales of the Token,
* if such sales are allowed. Valid values for this field are between 0 and
* 50000 inclusive, allowing transfer rates between 0.000% and 50.000% in
* increments of 0.001%. This field must NOT be present if the
* `tfTransferable` flag is not set.
*/
TransferFee?: number
/**
* URI that points to the data and/or metadata associated with the NFT.
* This field need not be an HTTP or HTTPS URL; it could be an IPFS URI, a
* magnet link, immediate data encoded as an RFC2379 "data" URL, or even an
* opaque issuer-specific encoding. The URI is NOT checked for validity, but
* the field is limited to a maximum length of 256 bytes.
*
* This field must be hex-encoded. You can use `convertStringToHex` to
* convert this field to the proper encoding.
*/
URI?: string
Flags?: number | NFTokenMintFlagsInterface
}
/**
* Verify the form and type of an NFTokenMint at runtime.
*
* @param tx - An NFTokenMint Transaction.
* @throws When the NFTokenMint is Malformed.
*/
export function validateNFTokenMint(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.Account === tx.Issuer) {
throw new ValidationError(
'NFTokenMint: Issuer must not be equal to Account',
)
}
if (tx.TokenTaxon == null) {
throw new ValidationError('NFTokenMint: missing field TokenTaxon')
}
}

View File

@@ -40,6 +40,10 @@ export enum AccountSetAsfFlags {
asfDefaultRipple = 8,
/** Enable Deposit Authorization on this account. */
asfDepositAuth = 9,
/**
* Allow another account to mint and burn tokens on behalf of this account.
*/
asfAuthorizedMinter = 10,
}
/**
@@ -135,6 +139,11 @@ export interface AccountSet extends BaseTransaction {
* digits. Valid values are 3 to 15 inclusive, or 0 to disable.
*/
TickSize?: number
/**
* Sets an alternate account that is allowed to mint NFTokens on this
* account's behalf using NFTokenMint's `Issuer` field.
*/
Minter?: string
}
const MIN_TICK_SIZE = 3

View File

@@ -1,31 +1,11 @@
/* eslint-disable max-lines-per-function -- Necessary for validateBaseTransaction */
/* eslint-disable complexity -- Necessary for validateBaseTransaction */
/* eslint-disable max-statements -- Necessary for validateBaseTransaction */
import { ValidationError } from '../../errors'
import { Memo, Signer } from '../common'
import { onlyHasFields } from '../utils'
import { TRANSACTION_TYPES } from 'ripple-binary-codec'
const transactionTypes = [
'AccountSet',
'AccountDelete',
'CheckCancel',
'CheckCash',
'CheckCreate',
'DepositPreauth',
'EscrowCancel',
'EscrowCreate',
'EscrowFinish',
'OfferCancel',
'OfferCreate',
'Payment',
'PaymentChannelClaim',
'PaymentChannelCreate',
'PaymentChannelFund',
'SetRegularKey',
'SignerListSet',
'TicketCreate',
'TrustSet',
]
import { ValidationError } from '../../errors'
import { Amount, IssuedCurrencyAmount, Memo, Signer } from '../common'
import { onlyHasFields } from '../utils'
const MEMO_SIZE = 3
@@ -72,18 +52,25 @@ function isSigner(obj: unknown): boolean {
const ISSUED_CURRENCY_SIZE = 3
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
}
/**
* Verify the form and type of an IssuedCurrencyAmount at runtime.
*
* @param obj - The object to check the form and type of.
* @param input - The input to check the form and type of.
* @returns Whether the IssuedCurrencyAmount is malformed.
*/
export function isIssuedCurrency(obj: Record<string, unknown>): boolean {
export function isIssuedCurrency(
input: unknown,
): input is IssuedCurrencyAmount {
return (
Object.keys(obj).length === ISSUED_CURRENCY_SIZE &&
typeof obj.value === 'string' &&
typeof obj.issuer === 'string' &&
typeof obj.currency === 'string'
isRecord(input) &&
Object.keys(input).length === ISSUED_CURRENCY_SIZE &&
typeof input.value === 'string' &&
typeof input.issuer === 'string' &&
typeof input.currency === 'string'
)
}
@@ -93,12 +80,8 @@ export function isIssuedCurrency(obj: Record<string, unknown>): boolean {
* @param amount - The object to check the form and type of.
* @returns Whether the Amount is malformed.
*/
export function isAmount(amount: unknown): boolean {
return (
typeof amount === 'string' ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS
isIssuedCurrency(amount as Record<string, unknown>)
)
export function isAmount(amount: unknown): amount is Amount {
return typeof amount === 'string' || isIssuedCurrency(amount)
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- no global flags right now, so this is fine
@@ -203,7 +186,7 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
throw new ValidationError('BaseTransaction: TransactionType not string')
}
if (!transactionTypes.includes(common.TransactionType)) {
if (!TRANSACTION_TYPES.includes(common.TransactionType)) {
throw new ValidationError('BaseTransaction: Unknown TransactionType')
}
@@ -270,3 +253,19 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
throw new ValidationError('BaseTransaction: invalid TxnSignature')
}
}
/**
* Parse the value of an amount, expressed either in XRP or as an Issued Currency, into a number.
*
* @param amount - An Amount to parse for its value.
* @returns The parsed amount value, or NaN if the amount count not be parsed.
*/
export function parseAmountValue(amount: unknown): number {
if (!isAmount(amount)) {
return NaN
}
if (typeof amount === 'string') {
return parseFloat(amount)
}
return parseFloat(amount.value)
}

View File

@@ -14,6 +14,19 @@ export { DepositPreauth } from './depositPreauth'
export { EscrowCancel } from './escrowCancel'
export { EscrowCreate } from './escrowCreate'
export { EscrowFinish } from './escrowFinish'
export { NFTokenAcceptOffer } from './NFTokenAcceptOffer'
export { NFTokenBurn } from './NFTokenBurn'
export { NFTokenCancelOffer } from './NFTokenCancelOffer'
export {
NFTokenCreateOffer,
NFTokenCreateOfferFlags,
NFTokenCreateOfferFlagsInterface,
} from './NFTokenCreateOffer'
export {
NFTokenMint,
NFTokenMintFlags,
NFTokenMintFlagsInterface,
} from './NFTokenMint'
export { OfferCancel } from './offerCancel'
export {
OfferCreateFlags,

View File

@@ -17,6 +17,20 @@ import { EscrowCancel, validateEscrowCancel } from './escrowCancel'
import { EscrowCreate, validateEscrowCreate } from './escrowCreate'
import { EscrowFinish, validateEscrowFinish } from './escrowFinish'
import { TransactionMetadata } from './metadata'
import {
NFTokenAcceptOffer,
validateNFTokenAcceptOffer,
} from './NFTokenAcceptOffer'
import { NFTokenBurn, validateNFTokenBurn } from './NFTokenBurn'
import {
NFTokenCancelOffer,
validateNFTokenCancelOffer,
} from './NFTokenCancelOffer'
import {
NFTokenCreateOffer,
validateNFTokenCreateOffer,
} from './NFTokenCreateOffer'
import { NFTokenMint, validateNFTokenMint } from './NFTokenMint'
import { OfferCancel, validateOfferCancel } from './offerCancel'
import { OfferCreate, validateOfferCreate } from './offerCreate'
import { Payment, validatePayment } from './payment'
@@ -50,6 +64,11 @@ export type Transaction =
| EscrowCancel
| EscrowCreate
| EscrowFinish
| NFTokenAcceptOffer
| NFTokenBurn
| NFTokenCancelOffer
| NFTokenCreateOffer
| NFTokenMint
| OfferCancel
| OfferCreate
| Payment
@@ -124,6 +143,26 @@ export function validate(transaction: Record<string, unknown>): void {
validateEscrowFinish(tx)
break
case 'NFTokenAcceptOffer':
validateNFTokenAcceptOffer(tx)
break
case 'NFTokenBurn':
validateNFTokenBurn(tx)
break
case 'NFTokenCancelOffer':
validateNFTokenCancelOffer(tx)
break
case 'NFTokenCreateOffer':
validateNFTokenCreateOffer(tx)
break
case 'NFTokenMint':
validateNFTokenMint(tx)
break
case 'OfferCancel':
validateOfferCancel(tx)
break

View File

@@ -0,0 +1,178 @@
import { assert } from 'chai'
import { validate, ValidationError } from 'xrpl-local'
const BUY_OFFER =
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AF'
const SELL_OFFER =
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE'
/**
* NFTokenAcceptOffer Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenAcceptOffer', function () {
it(`verifies valid NFTokenAcceptOffer with BuyOffer`, function () {
const validNFTokenAcceptOffer = {
TransactionType: 'NFTokenAcceptOffer',
BuyOffer: BUY_OFFER,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenAcceptOffer))
})
it(`verifies valid NFTokenAcceptOffer with SellOffer`, function () {
const validNFTokenAcceptOffer = {
TransactionType: 'NFTokenAcceptOffer',
SellOffer: SELL_OFFER,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenAcceptOffer))
})
it(`throws w/ missing SellOffer and BuyOffer`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: must set either SellOffer or BuyOffer',
)
})
it(`throws w/ missing SellOffer and present BrokerFee`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
BuyOffer: BUY_OFFER,
BrokerFee: '1',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: both SellOffer and BuyOffer must be set if using brokered mode',
)
})
it(`throws w/ missing BuyOffer and present BrokerFee`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
SellOffer: SELL_OFFER,
BrokerFee: '1',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: both SellOffer and BuyOffer must be set if using brokered mode',
)
})
it(`verifies valid NFTokenAcceptOffer with both offers and no BrokerFee`, function () {
const validNFTokenAcceptOffer = {
TransactionType: 'NFTokenAcceptOffer',
SellOffer: SELL_OFFER,
BuyOffer: BUY_OFFER,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenAcceptOffer))
})
it(`verifies valid NFTokenAcceptOffer with BrokerFee`, function () {
const validNFTokenAcceptOffer = {
TransactionType: 'NFTokenAcceptOffer',
SellOffer: SELL_OFFER,
BuyOffer: BUY_OFFER,
BrokerFee: '1',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenAcceptOffer))
})
it(`throws w/ BrokerFee === 0`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
SellOffer: SELL_OFFER,
BuyOffer: BUY_OFFER,
BrokerFee: '0',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: BrokerFee must be greater than 0; omit if there is no fee',
)
})
it(`throws w/ BrokerFee < 0`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
SellOffer: SELL_OFFER,
BuyOffer: BUY_OFFER,
BrokerFee: '-1',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: BrokerFee must be greater than 0; omit if there is no fee',
)
})
it(`throws w/ invalid BrokerFee`, function () {
const invalid = {
TransactionType: 'NFTokenAcceptOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
SellOffer: SELL_OFFER,
BuyOffer: BUY_OFFER,
BrokerFee: 1,
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenAcceptOffer: invalid BrokerFee',
)
})
})

View File

@@ -0,0 +1,41 @@
import { assert } from 'chai'
import { validate, ValidationError } from 'xrpl-local'
const TOKEN_ID =
'00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003'
/**
* NFTokenBurn Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenBurn', function () {
it(`verifies valid NFTokenBurn`, function () {
const validNFTokenBurn = {
TransactionType: 'NFTokenBurn',
TokenID: TOKEN_ID,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenBurn))
})
it(`throws w/ missing TokenID`, function () {
const invalid = {
TransactionType: 'NFTokenBurn',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenBurn: missing field TokenID',
)
})
})

View File

@@ -0,0 +1,58 @@
import { assert } from 'chai'
import { validate, ValidationError } from 'xrpl-local'
const BUY_OFFER =
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AF'
/**
* NFTokenCancelOffer Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenCancelOffer', function () {
it(`verifies valid NFTokenCancelOffer`, function () {
const validNFTokenCancelOffer = {
TransactionType: 'NFTokenCancelOffer',
TokenOffers: [BUY_OFFER],
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.doesNotThrow(() => validate(validNFTokenCancelOffer))
})
it(`throws w/ missing TokenOffers`, function () {
const invalid = {
TransactionType: 'NFTokenCancelOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCancelOffer: missing field TokenOffers',
)
})
it(`throws w/ empty TokenOffers`, function () {
const invalid = {
TransactionType: 'NFTokenCancelOffer',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TokenOffers: [],
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCancelOffer: empty field TokenOffers',
)
})
})

View File

@@ -0,0 +1,214 @@
import { assert } from 'chai'
import { validate, ValidationError, NFTokenCreateOfferFlags } from 'xrpl-local'
const TOKEN_ID =
'00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003'
/**
* NFTokenCreateOffer Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenCreateOffer', function () {
it(`verifies valid NFTokenCreateOffer buyside`, function () {
const validNFTokenCreateOffer = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: '1',
Owner: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Expiration: 1000,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.doesNotThrow(() => validate(validNFTokenCreateOffer))
})
it(`verifies valid NFTokenCreateOffer sellside`, function () {
const validNFTokenCreateOffer = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: '1',
Flags: NFTokenCreateOfferFlags.tfSellToken,
Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.doesNotThrow(() => validate(validNFTokenCreateOffer))
})
it(`verifies w/ 0 Amount NFTokenCreateOffer sellside`, function () {
const validNFTokenCreateOffer = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: '0',
Flags: NFTokenCreateOfferFlags.tfSellToken,
Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.doesNotThrow(() => validate(validNFTokenCreateOffer))
})
it(`throws w/ Account === Owner`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: '1',
Expiration: 1000,
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: Owner and Account must not be equal',
)
})
it(`throws w/ Account === Destination`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: '1',
Flags: NFTokenCreateOfferFlags.tfSellToken,
Expiration: 1000,
Destination: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: Destination and Account must not be equal',
)
})
it(`throws w/out TokenID`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
Amount: '1',
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe',
Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: missing field TokenID',
)
})
it(`throws w/ invalid Amount`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
TokenID: TOKEN_ID,
Amount: 1,
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe',
Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: invalid Amount',
)
})
it(`throws w/ missing Amount`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe',
Expiration: 1000,
TokenID: TOKEN_ID,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: invalid Amount',
)
})
it(`throws w/ Owner for sell offer`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
Expiration: 1000,
Owner: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TokenID: TOKEN_ID,
Flags: NFTokenCreateOfferFlags.tfSellToken,
Amount: '1',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: Owner must not be present for sell offers',
)
})
it(`throws w/out Owner for buy offer`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
Expiration: 1000,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Amount: '1',
TokenID: TOKEN_ID,
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: Owner must be present for buy offers',
)
})
it(`throws w/ 0 Amount for buy offer`, function () {
const invalid = {
TransactionType: 'NFTokenCreateOffer',
Expiration: 1000,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Owner: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Amount: '0',
Fee: '5000000',
TokenID: TOKEN_ID,
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenCreateOffer: Amount must be greater than 0 for buy offers',
)
})
})

View File

@@ -0,0 +1,69 @@
import { assert } from 'chai'
import {
convertStringToHex,
validate,
ValidationError,
NFTokenMintFlags,
} from 'xrpl-local'
/**
* NFTokenMint Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenMint', function () {
it(`verifies valid NFTokenMint`, function () {
const validNFTokenMint = {
TransactionType: 'NFTokenMint',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: NFTokenMintFlags.tfTransferable,
TokenTaxon: 0,
Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
TransferFee: 1,
URI: convertStringToHex('http://xrpl.org'),
} as any
assert.doesNotThrow(() => validate(validNFTokenMint))
})
it(`throws w/ missing TokenTaxon`, function () {
const invalid = {
TransactionType: 'NFTokenMint',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: NFTokenMintFlags.tfTransferable,
Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
TransferFee: 1,
URI: convertStringToHex('http://xrpl.org'),
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenMint: missing field TokenTaxon',
)
})
it(`throws w/ Account === Issuer`, function () {
const invalid = {
TransactionType: 'NFTokenMint',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: NFTokenMintFlags.tfTransferable,
Issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: 1,
TokenTaxon: 0,
URI: convertStringToHex('http://xrpl.org'),
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenMint: Issuer must not be equal to Account',
)
})
})