support DynamicNFT (#2726)

* support DynamicNFT

* Update history.md

* use xrpl-codec-gen to generate definitions.json and modify ripple-binary-code HISTORY.md

* use validateRequiredField for NFTokenID check

* move comment to a proper place

* Add some comment and modify integration test

* update transaction number

* update ci rippled version
This commit is contained in:
yinyiqian1
2025-02-03 10:32:30 -05:00
committed by GitHub
parent abdb192c69
commit 23d26c8c2e
11 changed files with 229 additions and 1 deletions

View File

@@ -188,3 +188,4 @@ fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2
DynamicNFT

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
RIPPLED_DOCKER_IMAGE: rippleci/rippled:develop
on:
push:

View File

@@ -11,6 +11,7 @@
### Added
* Support for the Price Oracles amendment (XLS-47).
* Add `NFTokenModify` transaction and add `tfMutable` flag in `NFTokenMint`
### Fixed
* Better error handling/error messages for serialization/deserialization errors.

View File

@@ -3085,6 +3085,7 @@
"NFTokenCancelOffer": 28,
"NFTokenCreateOffer": 27,
"NFTokenMint": 25,
"NFTokenModify": 61,
"OfferCancel": 8,
"OfferCreate": 7,
"OracleDelete": 52,

View File

@@ -17,6 +17,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
* New `MPTAmount` type support for `Payment` and `Clawback` transactions
* `parseTransactionFlags` as a utility function in the xrpl package to streamline transactions flags-to-map conversion
* Support for XLS-70d (Credentials)
* Add `NFTokenModify` transaction and add `tfMutable` flag in `NFTokenMint`
### Fixed
* `TransactionStream` model supports APIv2

View File

@@ -38,6 +38,10 @@ export enum NFTokenMintFlags {
* issuer.
*/
tfTransferable = 0x00000008,
/**
* If set, indicates that this NFT's URI can be modified.
*/
tfMutable = 0x00000010,
}
/**
@@ -51,6 +55,7 @@ export interface NFTokenMintFlagsInterface extends GlobalFlags {
tfOnlyXRP?: boolean
tfTrustLine?: boolean
tfTransferable?: boolean
tfMutable?: boolean
}
/**

View File

@@ -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.
* Can be omitted if the owner is the account submitting this transaction
*/
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.
*/
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')
}
}
}

View File

@@ -71,6 +71,7 @@ export {
NFTokenMintFlags,
NFTokenMintFlagsInterface,
} from './NFTokenMint'
export { NFTokenModify, validateNFTokenModify } from './NFTokenModify'
export { OfferCancel } from './offerCancel'
export {
OfferCreateFlags,

View File

@@ -57,6 +57,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'
@@ -143,6 +144,7 @@ export type SubmittableTransaction =
| NFTokenCancelOffer
| NFTokenCreateOffer
| NFTokenMint
| NFTokenModify
| OfferCancel
| OfferCreate
| OracleDelete
@@ -377,6 +379,10 @@ export function validate(transaction: Record<string, unknown>): void {
validateNFTokenMint(tx)
break
case 'NFTokenModify':
validateNFTokenModify(tx)
break
case 'OfferCancel':
validateOfferCancel(tx)
break

View File

@@ -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.youtube.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)
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,
)
})

View File

@@ -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',
)
})
})