diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index b834707f69..42db3cfce5 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -4,6 +4,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ## Unreleased Changes +### Added +* Support for XLS-52 (NFTokenMintOffer) + ## 4.1.0 (2024-12-23) ### Added diff --git a/packages/xrpl/src/models/transactions/NFTokenMint.ts b/packages/xrpl/src/models/transactions/NFTokenMint.ts index 2630a6b9c6..4745ebfd7c 100644 --- a/packages/xrpl/src/models/transactions/NFTokenMint.ts +++ b/packages/xrpl/src/models/transactions/NFTokenMint.ts @@ -1,4 +1,5 @@ import { ValidationError } from '../../errors' +import { Amount } from '../common' import { isHex } from '../utils' import { @@ -6,6 +7,8 @@ import { BaseTransaction, GlobalFlags, isAccount, + isAmount, + isNumber, validateBaseTransaction, validateOptionalField, } from './common' @@ -99,12 +102,34 @@ export interface NFTokenMint extends BaseTransaction { * set to `undefined` value if you do not use it. */ URI?: string | null + /** + * Indicates the amount for the Token. + * + * The amount can be zero. This would indicate that the account is giving + * the token away free, either to anyone at all, or to the account identified + * by the Destination field. + */ + Amount?: Amount + /** + * 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?: Account Flags?: number | NFTokenMintFlagsInterface } export interface NFTokenMintMetadata extends TransactionMetadataBase { // rippled 1.11.0 or later nftoken_id?: string + // if Amount is present + offer_id?: string } /** @@ -135,4 +160,16 @@ export function validateNFTokenMint(tx: Record): void { if (tx.NFTokenTaxon == null) { throw new ValidationError('NFTokenMint: missing field NFTokenTaxon') } + + if (tx.Amount == null) { + if (tx.Expiration != null || tx.Destination != null) { + throw new ValidationError( + 'NFTokenMint: Amount is required when Expiration or Destination is present', + ) + } + } + + validateOptionalField(tx, 'Amount', isAmount) + validateOptionalField(tx, 'Expiration', isNumber) + validateOptionalField(tx, 'Destination', isAccount) } diff --git a/packages/xrpl/test/integration/transactions/nftokenMint.test.ts b/packages/xrpl/test/integration/transactions/nftokenMint.test.ts index daf1f29c00..b12ff59966 100644 --- a/packages/xrpl/test/integration/transactions/nftokenMint.test.ts +++ b/packages/xrpl/test/integration/transactions/nftokenMint.test.ts @@ -6,6 +6,9 @@ import { NFTokenMint, TransactionMetadata, TxRequest, + unixTimeToRippleTime, + Wallet, + xrpToDrops, } from '../../../src' import { hashSignedTx } from '../../../src/utils/hashes' import serverUrl from '../serverUrl' @@ -14,16 +17,18 @@ import { teardownClient, type XrplIntegrationTestContext, } from '../setup' -import { testTransaction } from '../utils' +import { generateFundedWallet, testTransaction } from '../utils' // how long before each test case times out const TIMEOUT = 20000 describe('NFTokenMint', function () { let testContext: XrplIntegrationTestContext + let destinationWallet: Wallet beforeEach(async () => { testContext = await setupClient(serverUrl) + destinationWallet = await generateFundedWallet(testContext.client) }) afterEach(async () => teardownClient(testContext)) @@ -91,4 +96,63 @@ describe('NFTokenMint', function () { }, TIMEOUT, ) + + it( + 'test with Amount', + async function () { + const tx: NFTokenMint = { + TransactionType: 'NFTokenMint', + Account: testContext.wallet.address, + URI: convertStringToHex('https://www.google.com'), + NFTokenTaxon: 0, + Amount: xrpToDrops(1), + Expiration: unixTimeToRippleTime(Date.now() + 1000 * 60 * 60 * 24), + Destination: destinationWallet.address, + } + const response = await testTransaction( + testContext.client, + tx, + testContext.wallet, + ) + assert.equal(response.type, 'response') + + const txRequest: TxRequest = { + command: 'tx', + transaction: hashSignedTx(response.result.tx_blob), + } + const txResponse = await testContext.client.request(txRequest) + + assert.equal( + (txResponse.result.meta as TransactionMetadata).TransactionResult, + 'tesSUCCESS', + ) + + const nftokenID = + getNFTokenID( + txResponse.result.meta as TransactionMetadata, + ) ?? 'undefined' + + const nftokenOfferID = ( + txResponse.result.meta as TransactionMetadata + ).offer_id + + const sellOffers = await testContext.client.request({ + command: 'nft_sell_offers', + nft_id: nftokenID, + }) + + const existsOffer = sellOffers.result.offers.some( + (value) => value.nft_offer_index === nftokenOfferID, + ) + + assert.isTrue( + existsOffer, + `Expected to exist an offer for NFT with NFTokenID ${nftokenID} but did not find it. + \n\nHere's what was returned from 'nft_sell_offers': ${JSON.stringify( + sellOffers, + )}`, + ) + }, + TIMEOUT, + ) }) diff --git a/packages/xrpl/test/models/NFTokenMint.test.ts b/packages/xrpl/test/models/NFTokenMint.test.ts index 82dbd001f5..3a5d0cf746 100644 --- a/packages/xrpl/test/models/NFTokenMint.test.ts +++ b/packages/xrpl/test/models/NFTokenMint.test.ts @@ -109,4 +109,56 @@ describe('NFTokenMint', function () { 'NFTokenMint: URI must be in hex format', ) }) + + it(`throws when Amount is null but Expiration is present`, function () { + const invalid = { + TransactionType: 'NFTokenMint', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Fee: '5000000', + Sequence: 2470665, + NFTokenTaxon: 0, + Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', + Expiration: 123456, + } as any + + assert.throws( + () => validate(invalid), + ValidationError, + 'NFTokenMint: Amount is required when Expiration or Destination is present', + ) + }) + + it(`throws when Amount is null but Destination is present`, function () { + const invalid = { + TransactionType: 'NFTokenMint', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Fee: '5000000', + Sequence: 2470665, + NFTokenTaxon: 0, + Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', + Destination: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + } as any + + assert.throws( + () => validate(invalid), + ValidationError, + 'NFTokenMint: Amount is required when Expiration or Destination is present', + ) + }) + + it(`verifies valid NFTokenMint with Amount, Destination and Expiration`, function () { + const valid = { + TransactionType: 'NFTokenMint', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Fee: '5000000', + Sequence: 2470665, + NFTokenTaxon: 0, + Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', + Amount: '1000000', + Destination: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + Expiration: 123456, + } as any + + assert.doesNotThrow(() => validate(valid)) + }) })