Skip to content

support DynamicNFT #2726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 3, 2025
1 change: 1 addition & 0 deletions packages/ripple-binary-codec/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Added
* Support for the Price Oracles amendment (XLS-47).
* Add `NFTokenModify` transaction and add `tfMutable` flag in `NFTokenMint`
Copy link
Collaborator

@achowdhry-ripple achowdhry-ripple Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can follow the format on line 13 and indicate XLS-46 instead of adding the specifics here


### Fixed
* Better error handling/error messages for serialization/deserialization errors.
Expand Down
1 change: 1 addition & 0 deletions packages/ripple-binary-codec/src/enums/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2974,6 +2974,7 @@
"DIDDelete": 50,
"OracleSet": 51,
"OracleDelete": 52,
"NFTokenModify": 53,
"EnableAmendment": 100,
"SetFee": 101,
"UNLModify": 102
Expand Down
1 change: 1 addition & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr

### Added
* Add `nfts_by_issuer` clio-only API definition
* Add `NFTokenModify` transaction and add `tfMutable` flag in `NFTokenMint`

## 3.1.0 (2024-06-03)

Expand Down
5 changes: 5 additions & 0 deletions packages/xrpl/src/models/transactions/NFTokenMint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export enum NFTokenMintFlags {
* issuer.
*/
tfTransferable = 0x00000008,
/**
* If set, indicates that this NFT's URI can be modified.
*/
tfMutable = 0x00000010,
}

/**
Expand All @@ -51,6 +55,7 @@ export interface NFTokenMintFlagsInterface extends GlobalFlags {
tfOnlyXRP?: boolean
tfTrustLine?: boolean
tfTransferable?: boolean
tfMutable?: boolean
}

/**
Expand Down
67 changes: 67 additions & 0 deletions packages/xrpl/src/models/transactions/NFTokenModify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ValidationError } from '../../errors'
import { isHex } from '../utils'

import {
BaseTransaction,
validateBaseTransaction,
isAccount,
isString,
validateOptionalField,
Account,
validateRequiredField,
} from './common'

/**
* The NFTokenModify transaction modifies an NFToken's URI
* if its tfMutable is set to true.
*/
export interface NFTokenModify extends BaseTransaction {
TransactionType: 'NFTokenModify'
/**
* Identifies the NFTokenID of the NFToken object that the
* offer references.
*/
NFTokenID: string
/**
* Indicates the AccountID of the account that owns the
* corresponding NFToken.
*/
Owner?: Account
/**
* 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.
*
* This field must not be an empty string. Omit it from the transaction or
* set to `undefined` value if you do not use it.
Copy link
Collaborator

@achowdhry-ripple achowdhry-ripple Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this comment reference setting to null for clarity

*/
URI?: string | null
}

/**
* Verify the form and type of an NFTokenModify at runtime.
*
* @param tx - An NFTokenModify Transaction.
* @throws When the NFTokenModify is Malformed.
*/
export function validateNFTokenModify(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)

validateRequiredField(tx, 'NFTokenID', isString)
validateOptionalField(tx, 'Owner', isAccount)
validateOptionalField(tx, 'URI', isString)

if (tx.URI !== undefined && typeof tx.URI === 'string') {
if (tx.URI === '') {
throw new ValidationError('NFTokenModify: URI must not be empty string')
}
if (!isHex(tx.URI)) {
throw new ValidationError('NFTokenModify: URI must be in hex format')
}
}
}
1 change: 1 addition & 0 deletions packages/xrpl/src/models/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export {
NFTokenMintFlags,
NFTokenMintFlagsInterface,
} from './NFTokenMint'
export { NFTokenModify, validateNFTokenModify } from './NFTokenModify'
export { OfferCancel } from './offerCancel'
export {
OfferCreateFlags,
Expand Down
6 changes: 6 additions & 0 deletions packages/xrpl/src/models/transactions/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
validateNFTokenCreateOffer,
} from './NFTokenCreateOffer'
import { NFTokenMint, validateNFTokenMint } from './NFTokenMint'
import { NFTokenModify, validateNFTokenModify } from './NFTokenModify'
import { OfferCancel, validateOfferCancel } from './offerCancel'
import { OfferCreate, validateOfferCreate } from './offerCreate'
import { OracleDelete, validateOracleDelete } from './oracleDelete'
Expand Down Expand Up @@ -120,6 +121,7 @@ export type SubmittableTransaction =
| NFTokenCancelOffer
| NFTokenCreateOffer
| NFTokenMint
| NFTokenModify
| OfferCancel
| OfferCreate
| OracleDelete
Expand Down Expand Up @@ -326,6 +328,10 @@ export function validate(transaction: Record<string, unknown>): void {
validateNFTokenMint(tx)
break

case 'NFTokenModify':
validateNFTokenModify(tx)
break

case 'OfferCancel':
validateOfferCancel(tx)
break
Expand Down
104 changes: 104 additions & 0 deletions packages/xrpl/test/integration/transactions/nftokenModify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { assert } from 'chai'

import { NFTokenModify } from '../../../dist/npm'
import { NFTokenMintFlags } from '../../../dist/npm/src'
import {
convertStringToHex,
getNFTokenID,
NFTokenMint,
TransactionMetadata,
TxRequest,
} from '../../../src'
import { hashSignedTx } from '../../../src/utils/hashes'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'

// how long before each test case times out
const TIMEOUT = 20000

describe('NFTokenModify', function () {
let testContext: XrplIntegrationTestContext

beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))

// Mint an NFToken with tfMutable flag and modify URI later
it(
'modify NFToken URI',
async function () {
const oldUri = convertStringToHex('https://www.google.com')
const newUri = convertStringToHex('https://www.ripple.com')

const mutableMint: NFTokenMint = {
TransactionType: 'NFTokenMint',
Account: testContext.wallet.address,
Flags: NFTokenMintFlags.tfMutable,
URI: oldUri,
NFTokenTaxon: 0,
}
const response = await testTransaction(
testContext.client,
mutableMint,
testContext.wallet,
)
assert.equal(response.type, 'response')

const mutableTx: TxRequest = {
command: 'tx',
transaction: hashSignedTx(response.result.tx_blob),
}
const mutableTxResponse = await testContext.client.request(mutableTx)
Comment on lines +53 to +57
Copy link
Collaborator

@achowdhry-ripple achowdhry-ripple Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come we need a separate tx request here? could we get the nft id from the mint response

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mint response won't provide nftoken id. we have to query it through another request


const mutableNFTokenID =
getNFTokenID(
mutableTxResponse.result.meta as TransactionMetadata<NFTokenMint>,
) ?? 'undefined'

const accountNFTs = await testContext.client.request({
command: 'account_nfts',
account: testContext.wallet.address,
})

assert.equal(
accountNFTs.result.account_nfts.find(
(nft) => nft.NFTokenID === mutableNFTokenID,
)?.URI,
oldUri,
)

const modifyTx: NFTokenModify = {
TransactionType: 'NFTokenModify',
Account: testContext.wallet.address,
NFTokenID: mutableNFTokenID,
URI: newUri,
}

const modifyResponse = await testTransaction(
testContext.client,
modifyTx,
testContext.wallet,
)
assert.equal(modifyResponse.type, 'response')

const nfts = await testContext.client.request({
command: 'account_nfts',
account: testContext.wallet.address,
})

assert.equal(
nfts.result.account_nfts.find(
(nft) => nft.NFTokenID === mutableNFTokenID,
)?.URI,
newUri,
)
},
TIMEOUT,
)
})
41 changes: 41 additions & 0 deletions packages/xrpl/test/models/NFTokenModify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { assert } from 'chai'

import { convertStringToHex, validate, ValidationError } from '../../src'

const TOKEN_ID =
'00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003'

/**
* NFTokenModify Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenModify', function () {
it(`verifies valid NFTokenModify`, function () {
const validNFTokenModify = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
NFTokenID: TOKEN_ID,
Fee: '5000000',
Sequence: 2470665,
URI: convertStringToHex('http://xrpl.org'),
} as any

assert.doesNotThrow(() => validate(validNFTokenModify))
})

it(`throws w/ missing NFTokenID`, function () {
const invalid = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any

assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenModify: missing field NFTokenID',
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add test cases for invalid hex format and empty string check here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a separate PR to add the test and resolve comments: #2892

})
})
Loading