diff --git a/package-lock.json b/package-lock.json index 9ea2d4cdc..2743ac8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,7 @@ "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "xrpl": "^4.5.0" + "xrpl": "^5.1.0" }, "engines": { "node": ">=22.0.0 <23", @@ -4561,29 +4561,28 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@noble/curves/-/curves-2.4.0.tgz", + "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.4.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -5349,39 +5348,37 @@ "license": "MIT" }, "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "dev": true, + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/bip32/-/bip32-2.4.0.tgz", + "integrity": "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few==", "dev": true, "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/hashes": "2.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -6931,16 +6928,14 @@ "node": ">=18.0.0" } }, - "node_modules/@xrplf/isomorphic/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", + "node_modules/@xrplf/mpt-crypto": { + "version": "0.1.1", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@xrplf/mpt-crypto/-/mpt-crypto-0.1.1.tgz", + "integrity": "sha512-Wv9wbk60DRGWvI+B9DITwfWygCWDfEjv8yWxiLJyDf/G8UPkfhVlUKIflF9nMaBFMygPRkXrpO1jNqTj6/vJow==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">= 18" } }, "node_modules/@xrplf/prettier-config": { @@ -6951,14 +6946,14 @@ "license": "ISC" }, "node_modules/@xrplf/secret-numbers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-2.0.0.tgz", - "integrity": "sha512-z3AOibRTE9E8MbjgzxqMpG1RNaBhQ1jnfhNCa1cGf2reZUJzPMYs4TggQTc7j8+0WyV3cr7y/U8Oz99SXIkN5Q==", + "version": "3.0.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@xrplf/secret-numbers/-/secret-numbers-3.0.0.tgz", + "integrity": "sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA==", "dev": true, "license": "ISC", "dependencies": { - "@xrplf/isomorphic": "^1.0.1", - "ripple-keypairs": "^2.0.0" + "@xrplf/isomorphic": "^1.0.2", + "ripple-keypairs": "^3.0.0" } }, "node_modules/accepts": { @@ -7752,14 +7747,10 @@ } }, "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } + "version": "10.0.2", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -19676,19 +19667,10 @@ "node": ">= 18" } }, - "node_modules/ripple-address-codec/node_modules/@scure/base": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", - "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ripple-binary-codec": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.9.0.tgz", - "integrity": "sha512-DOv4CPwm2B5mjAJNHE4tfes7tIqefi85iun8BskS7/i1PvLIwVydHIbTV5hbQ7eT69Zg4K5dtO1CzfuvQVAYvQ==", + "version": "2.10.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/ripple-binary-codec/-/ripple-binary-codec-2.10.0.tgz", + "integrity": "sha512-H4pBm2WdOMmVnA0HGUdWAShmUFdmqavWAsi2WlMCzOknc1XjrZssa/RxEyziaPww8MXe1OpgGU7uCs8G2nYe/A==", "license": "ISC", "dependencies": { "@xrplf/isomorphic": "^1.0.2", @@ -19699,25 +19681,19 @@ "node": ">= 18" } }, - "node_modules/ripple-binary-codec/node_modules/bignumber.js": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", - "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", - "license": "MIT" - }, "node_modules/ripple-keypairs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-2.0.0.tgz", - "integrity": "sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag==", + "version": "3.0.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/ripple-keypairs/-/ripple-keypairs-3.0.0.tgz", + "integrity": "sha512-lE69pD0E8hFNCqZoVXRyY45Yi8Ku+Qw7Rf1qRwPj4nOi34vp9NAuwzfiJH1IwXGWNCfEkwVfctG99CPTEoUf+g==", "dev": true, "license": "ISC", "dependencies": { - "@noble/curves": "^1.0.0", - "@xrplf/isomorphic": "^1.0.0", - "ripple-address-codec": "^5.0.0" + "@noble/curves": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "ripple-address-codec": "^5.0.1" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/robust-predicates": { @@ -23639,25 +23615,26 @@ "license": "MIT" }, "node_modules/xrpl": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.6.0.tgz", - "integrity": "sha512-0nXZfqDHRJ6bsDv1WtA9MdCYalMtXuxVa9mtLdqT3xypRKf2LwT5DbuGL/kHcVfuqk3B+ly+SFARlrnX+LHtRQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-5.1.0.tgz", + "integrity": "sha512-wYTiyrFqV22kwSX2lrUCD10Ns8Zyv2zz8rZ2Pi0KOVm5Sc7/8m7rrca2dAZy1vGhOt36tkvWyX4aC2SQveyT9w==", "dev": true, "license": "ISC", "dependencies": { - "@scure/bip32": "^1.3.1", - "@scure/bip39": "^1.2.1", - "@xrplf/isomorphic": "^1.0.1", - "@xrplf/secret-numbers": "^2.0.0", - "bignumber.js": "^9.0.0", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "@xrplf/mpt-crypto": "^0.1.1", + "@xrplf/secret-numbers": "^3.0.0", + "bignumber.js": "^10.0.2", "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", - "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.7.0", - "ripple-keypairs": "^2.0.0" + "ripple-address-codec": "^5.0.1", + "ripple-binary-codec": "^2.10.0", + "ripple-keypairs": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0" } }, "node_modules/xrpl-client": { diff --git a/package.json b/package.json index cc8d56238..3c11e9c5e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "xrpl": "^4.5.0" + "xrpl": "^5.1.0" }, "resolutions": { "jest-environment-jsdom": "29.3.1", diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index bc4553c11..81dc72bba 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -945,5 +945,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index 8c55a6a7f..b0f84b37a 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -946,5 +946,11 @@ "registered": "Registered", "included": "Included", "maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).", - "maintenance_banner.countdown_prefix": "Starts in" + "maintenance_banner.countdown_prefix": "Starts in", + "can_enable": "Can Enable", + "can_enable_flag_tooltip": "The issuer can still enable this capability — it has not been permanently locked", + "immutable": "Immutable", + "immutable_flag_tooltip": "This capability has been permanently locked by the issuer and cannot be enabled", + "immutable_field_tooltip": "This field has been permanently locked by the issuer and cannot be changed", + "immutable_flags": "Immutable Flags" } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 12e1a48f4..e30c9f535 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -946,5 +946,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index 99bb71f9e..2a5978c1d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -946,5 +946,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index 8229d9e98..b392093d2 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -946,5 +946,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index d02b1dd5c..5b974d392 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -946,5 +946,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json index c1ef818ab..567be79be 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -946,5 +946,11 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "immutable_flags": null, + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/scripts/showcase-dynamic-mpt.mjs b/scripts/showcase-dynamic-mpt.mjs new file mode 100644 index 000000000..1550bb645 --- /dev/null +++ b/scripts/showcase-dynamic-mpt.mjs @@ -0,0 +1,281 @@ +#!/usr/bin/env node +/** + * showcase-dynamic-mpt.mjs + * + * Submits four DynamicMPT transactions to a local rippled standalone and prints + * Explorer URLs to review each UI surface: + * 1. MPTokenIssuanceCreate — all mutable (no ImmutableFlags) + * 2. MPTokenIssuanceCreate — with ImmutableFlags set + * 3. MPTokenIssuanceSet — enable capabilities via tfMPTSetCan* Flags + * 4. MPTokenIssuanceSet — update TransferFee + lock flags via ImmutableFlags + * + * Usage: + * node scripts/showcase-dynamic-mpt.mjs + * EXPLORER=http://localhost:3001/localhost node scripts/showcase-dynamic-mpt.mjs + */ + +const ENDPOINT = process.env.RIPPLED ?? 'http://localhost:5005' +const GENESIS_ACCOUNT = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' +const GENESIS_SECRET = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' +const EXPLORER = process.env.EXPLORER ?? 'http://localhost:3001/localhost' + +// ── RPC helpers ─────────────────────────────────────────────────────────────── + +async function rpc(method, params = {}) { + const res = await fetch(ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method, params: [params] }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const { result } = await res.json() + if (result.error) + throw new Error(`${result.error}: ${result.error_message ?? ''}`) + return result +} + +async function submit(txJson) { + const result = await rpc('submit', { + tx_json: txJson, + secret: GENESIS_SECRET, + }) + const eng = result.engine_result + if (eng !== 'tesSUCCESS' && eng !== 'terQUEUED') { + throw new Error(`${eng}: ${result.engine_result_message}`) + } + return result.tx_json.hash +} + +async function advanceLedger() { + await rpc('ledger_accept') +} + +async function getValidatedTx(hash) { + // retry a few times in case the ledger hasn't closed yet + for (let i = 0; i < 5; i++) { + try { + const result = await rpc('tx', { transaction: hash }) + if (result.validated) return result + } catch (_) {} + await new Promise((r) => setTimeout(r, 200)) + } + throw new Error(`tx ${hash} not validated after retries`) +} + +// ── XLS-0089 MPT metadata schemas ──────────────────────────────────────────── +// Short key names per spec: t=ticker, n=name, d=desc, i=icon, ac=asset_class, +// as=asset_subclass (required for ac=rwa), in=issuer_name, us=uris, ai=additional_info + +// Issuance A: DeFi token, all capabilities still mutable at creation. +const METADATA_A = { + t: 'DYNX', + n: 'DynToken Flex', + d: 'A DeFi token demonstrating DynamicMPT — all capabilities mutable at creation. The issuer can still enable escrow, trade, clawback, and more via MPTokenIssuanceSet.', + i: 'https://example.org/dynx/icon.png', + ac: 'defi', + in: 'XRPL Foundation', +} + +// Issuance B: RWA (treasury) token with CanLock, RequireAuth, and Metadata +// permanently locked at issuance via ImmutableFlags. +const METADATA_B = { + t: 'RTBIL', + n: 'T-Bill Yield Token', + d: 'Yield-bearing token backed by U.S. Treasuries. CanLock, RequireAuth, and Metadata are permanently immutable — set once at issuance and can never be changed.', + i: 'https://example.org/rtbil/icon.png', + ac: 'rwa', + as: 'treasury', + in: 'XRPL Foundation', + us: [{ u: 'https://example.org/rtbil', c: 'website', t: 'Product Page' }], + ai: { interest_rate: '5.00%', maturity_date: '2027-06-30' }, +} + +// TX4 update to Issuance A: new metadata after locking CanTrade, CanTransfer, +// and TransferFee — reflects the permanent change in the description. +const METADATA_A_UPDATED = { + t: 'DYNX', + n: 'DynToken Flex', + d: 'Updated via MPTokenIssuanceSet: TransferFee raised to 0.075%. CanTrade, CanTransfer, and TransferFee are now permanently immutable.', + i: 'https://example.org/dynx/icon.png', + ac: 'defi', + in: 'XRPL Foundation', + us: [{ u: 'https://example.org/dynx', c: 'website', t: 'Product Page' }], +} + +function toHex(obj) { + const json = typeof obj === 'string' ? obj : JSON.stringify(obj) + return Buffer.from(json, 'utf8').toString('hex').toUpperCase() +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +async function main() { + console.log(`Connecting to ${ENDPOINT} …`) + const { info } = await rpc('server_info') + console.log( + ` rippled ${info.build_version} network_id=${info.network_id}\n`, + ) + + const { account_data } = await rpc('account_info', { + account: GENESIS_ACCOUNT, + ledger_index: 'current', + }) + let seq = account_data.Sequence + console.log(` Genesis account: ${GENESIS_ACCOUNT} seq=${seq}\n`) + + const results = {} + + // ── TX 1: Create — all mutable (no ImmutableFlags) ────────────────────────── + // Flags: tfMPTCanTransfer (0x20) + // Metadata: XLS-0089 JSON (ac=defi), all capabilities mutable. + // Shows: Create Simple with NO "Immutable Flags" row. + // Settings: 7 "Can Enable" badges on unlocked disabled caps; + // Metadata + TransferFee field rows visible with no badge. + console.log('TX1 MPTokenIssuanceCreate — all mutable (DYNX / DeFi)') + const hash1 = await submit({ + TransactionType: 'MPTokenIssuanceCreate', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + Flags: 0x00000020, // tfMPTCanTransfer + AssetScale: 6, + MaximumAmount: '1000000000000', + TransferFee: 100, // 0.010% — still mutable + MPTokenMetadata: toHex(METADATA_A), + }) + await advanceLedger() + const tx1 = await getValidatedTx(hash1) + const issuanceIdA = tx1.meta?.mpt_issuance_id ?? '?' + results.tx1 = { hash: hash1, issuanceId: issuanceIdA } + console.log(` hash : ${hash1}`) + console.log(` issuance ID: ${issuanceIdA}\n`) + + // ── TX 2: Create — with ImmutableFlags ────────────────────────────────────── + // Flags: tfMPTCanTransfer (0x20) | tfMPTRequireAuth (0x04) + // ImmutableFlags: tifMPTCanLock(0x2) + tifMPTRequireAuth(0x4) + tifMPTMetadata(0x10000) + // = 0x10006 = 65542 + // Metadata: XLS-0089 JSON (ac=rwa, as=treasury), with URIs and ai fields. + // Shows: Create Simple "Immutable Flags" row listing tifMPTCanLock, + // tifMPTRequireAuth, tifMPTMetadata; metadata renders as JSON in Detail. + // Settings: CanLock + RequireAuth → "Immutable" badge; + // Metadata field row → "Immutable" badge; TransferFee row mutable. + console.log( + 'TX2 MPTokenIssuanceCreate — with ImmutableFlags (RTBIL / RWA treasury)', + ) + const hash2 = await submit({ + TransactionType: 'MPTokenIssuanceCreate', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + Flags: 0x00000024, // tfMPTCanTransfer | tfMPTRequireAuth + AssetScale: 2, + MaximumAmount: '9223372036854775807', + TransferFee: 500, // 0.500% + MPTokenMetadata: toHex(METADATA_B), + ImmutableFlags: 0x00010006, // tifMPTCanLock + tifMPTRequireAuth + tifMPTMetadata + }) + await advanceLedger() + const tx2 = await getValidatedTx(hash2) + const issuanceIdB = tx2.meta?.mpt_issuance_id ?? '?' + results.tx2 = { hash: hash2, issuanceId: issuanceIdB } + console.log(` hash : ${hash2}`) + console.log(` issuance ID: ${issuanceIdB}\n`) + + // ── TX 3: Set — enable capabilities via tfMPTSetCan* Flags ────────────────── + // Flags: tfMPTSetCanEscrow(0x10) | tfMPTSetCanTrade(0x20) = 0x30 + // Shows: Set Simple with NO ImmutableFlags row; the tfMPTSetCan* bits appear + // automatically in the Detail tab via TX_FLAGS.MPTokenIssuanceSet. + console.log( + 'TX3 MPTokenIssuanceSet — enable CanEscrow + CanTrade on Issuance A', + ) + const hash3 = await submit({ + TransactionType: 'MPTokenIssuanceSet', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + MPTokenIssuanceID: issuanceIdA, + Flags: 0x00000030, // tfMPTSetCanEscrow | tfMPTSetCanTrade + }) + await advanceLedger() + results.tx3 = { hash: hash3 } + console.log(` hash: ${hash3}\n`) + + // ── TX 4: Set — update TransferFee + metadata + lock via ImmutableFlags ────── + // ImmutableFlags: tifMPTCanTrade(0x10) + tifMPTCanTransfer(0x20) + tifMPTTransferFee(0x20000) + // = 0x20030 = 131120 + // Metadata: updated XLS-0089 JSON reflecting the new locked state. + // Shows: Set Simple with TransferFee row (0.075%), Metadata row (JSON rendered), + // and "Immutable Flags" row listing tifMPTCanTrade, tifMPTCanTransfer, + // tifMPTTransferFee. + console.log( + 'TX4 MPTokenIssuanceSet — update metadata + TransferFee + lock via ImmutableFlags on Issuance A', + ) + const hash4 = await submit({ + TransactionType: 'MPTokenIssuanceSet', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + MPTokenIssuanceID: issuanceIdA, + TransferFee: 750, // update: 0.010% → 0.075% + MPTokenMetadata: toHex(METADATA_A_UPDATED), + ImmutableFlags: 0x00020030, // tifMPTCanTrade + tifMPTCanTransfer + tifMPTTransferFee + }) + await advanceLedger() + results.tx4 = { hash: hash4 } + console.log(` hash: ${hash4}\n`) + + // ── Summary ────────────────────────────────────────────────────────────────── + console.log( + '═══════════════════════════════════════════════════════════════════', + ) + console.log(' EXPLORER URLS') + console.log( + '═══════════════════════════════════════════════════════════════════\n', + ) + + console.log( + '── MPTokenIssuanceCreate / Simple view ─────────────────────────────', + ) + console.log(' TX1 DYNX — no ImmutableFlags ("Immutable Flags" row absent)') + console.log(` ${EXPLORER}/transactions/${results.tx1.hash}/simple`) + console.log() + console.log( + ' TX2 RTBIL — ImmutableFlags=0x10006 (tifMPTCanLock, tifMPTRequireAuth, tifMPTMetadata)', + ) + console.log(` ${EXPLORER}/transactions/${results.tx2.hash}/simple`) + console.log() + + console.log( + '── MPTokenIssuanceSet / Simple view ────────────────────────────────', + ) + console.log( + ' TX3 tfMPTSetCanEscrow + tfMPTSetCanTrade (see Detail tab for flag decode)', + ) + console.log(` ${EXPLORER}/transactions/${results.tx3.hash}/simple`) + console.log() + console.log(' TX4 TransferFee + Metadata (JSON) + ImmutableFlags rows') + console.log(` ${EXPLORER}/transactions/${results.tx4.hash}/simple`) + console.log() + + console.log( + '── MPT token Settings page ─────────────────────────────────────────', + ) + console.log( + ' Issuance A DYNX — "Can Enable" on unlocked caps; CanTrade+CanTransfer+TransferFee → "Immutable"', + ) + console.log(` ${EXPLORER}/token/mpt/${results.tx1.issuanceId}`) + console.log() + console.log( + ' Issuance B RTBIL — CanLock+RequireAuth+Metadata → "Immutable"; TransferFee still mutable', + ) + console.log(` ${EXPLORER}/token/mpt/${results.tx2.issuanceId}`) + console.log() + console.log( + '═══════════════════════════════════════════════════════════════════', + ) +} + +main().catch((err) => { + console.error('\nFatal:', err.message) + process.exit(1) +}) diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx index fac9ee031..fa9fcca91 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -2,17 +2,39 @@ import { useTranslation } from 'react-i18next' interface Props { flags?: string[] + immutableFlags?: string[] } interface FlagItem { key: string label: string enabled: boolean + // The lsifMPT* flag name that permanently locks this capability (XLS-94). + immutableFlag?: string } -export const Settings = ({ flags = [] }: Props): JSX.Element => { +interface FieldItem { + key: string + label: string + // The lsifMPT* flag name that permanently locks this field (XLS-94). + immutableFlag: string +} + +export const Settings = ({ + flags = [], + immutableFlags = [], +}: Props): JSX.Element => { const { t } = useTranslation() + // Whether the lsif bit is present — the capability/field is permanently frozen. + const isLocked = (immutableFlag?: string): boolean => + !!immutableFlag && immutableFlags.includes(immutableFlag) + + // Whether the capability can still be enabled: not yet locked and not yet on. + // Absent immutableFlag means it is not a lockable capability (e.g. lsfMPTLocked). + const isCanEnable = (immutableFlag?: string, enabled = false): boolean => + !!immutableFlag && !immutableFlags.includes(immutableFlag) && !enabled + const flagItems: FlagItem[] = [ { key: 'locked', @@ -23,36 +45,58 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { key: 'canLock', label: t('can_lock'), enabled: flags.includes('lsfMPTCanLock'), + immutableFlag: 'lsifMPTCanLock', }, { key: 'requireAuth', label: t('require_auth'), enabled: flags.includes('lsfMPTRequireAuth'), + immutableFlag: 'lsifMPTRequireAuth', }, { key: 'canEscrow', label: t('can_escrow'), enabled: flags.includes('lsfMPTCanEscrow'), + immutableFlag: 'lsifMPTCanEscrow', }, { key: 'canTrade', label: t('can_trade'), enabled: flags.includes('lsfMPTCanTrade'), + immutableFlag: 'lsifMPTCanTrade', }, { key: 'canTransfer', label: t('can_transfer'), enabled: flags.includes('lsfMPTCanTransfer'), + immutableFlag: 'lsifMPTCanTransfer', }, { key: 'canClawback', label: t('can_clawback'), enabled: flags.includes('lsfMPTCanClawback'), + immutableFlag: 'lsifMPTCanClawback', }, { key: 'canConfidentialAmount', label: t('can_confidential_amount'), enabled: flags.includes('lsfMPTCanConfidentialAmount'), + immutableFlag: 'lsifMPTCanHoldConfidentialBalance', + }, + ] + + // Mutable fields (not capability flags): always shown. An Immutable badge + // appears once the issuer has permanently locked the field via ImmutableFlags. + const fieldItems: FieldItem[] = [ + { + key: 'metadata', + label: t('metadata'), + immutableFlag: 'lsifMPTMetadata', + }, + { + key: 'transferFee', + label: t('transfer_fee'), + immutableFlag: 'lsifMPTTransferFee', }, ] @@ -63,10 +107,56 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { {flagItems.map((flag) => (