Skip to content

Commit

Permalink
support DynamicNFT (#2726)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yinyiqian1 authored Feb 3, 2025
1 parent abdb192 commit 23d26c8
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 1 deletion.
1 change: 1 addition & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,4 @@ fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2
DynamicNFT
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/ripple-binary-codec/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 @@ -3085,6 +3085,7 @@
"NFTokenCancelOffer": 28,
"NFTokenCreateOffer": 27,
"NFTokenMint": 25,
"NFTokenModify": 61,
"OfferCancel": 8,
"OfferCreate": 7,
"OracleDelete": 52,
Expand Down
1 change: 1 addition & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.
* 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')
}
}
}
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 @@ -71,6 +71,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 @@ -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'
Expand Down Expand Up @@ -143,6 +144,7 @@ export type SubmittableTransaction =
| NFTokenCancelOffer
| NFTokenCreateOffer
| NFTokenMint
| NFTokenModify
| OfferCancel
| OfferCreate
| OracleDelete
Expand Down Expand Up @@ -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
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.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,
)
})
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',
)
})
})

0 comments on commit 23d26c8

Please sign in to comment.