From 587a3bbfa21d5d4115aa91b2b2d3ad0ef2e9d4b0 Mon Sep 17 00:00:00 2001 From: Jackson Mills Date: Tue, 12 Apr 2022 15:48:50 -0700 Subject: [PATCH] Add parseNFTokenID and tests (#1961) * Add parseNFTokenID and tests * Lint * Move parseNFTokenID to utils * Lint * Move the NFTokenID into models * Move NFTokenID type out of models * Lint * Remove extra type and lint --- packages/xrpl/HISTORY.md | 1 + packages/xrpl/src/utils/index.ts | 2 + packages/xrpl/src/utils/parseNFTokenID.ts | 81 ++++++++++++++++++++++ packages/xrpl/test/utils/parseNFTokenID.ts | 27 ++++++++ 4 files changed, 111 insertions(+) create mode 100644 packages/xrpl/src/utils/parseNFTokenID.ts create mode 100644 packages/xrpl/test/utils/parseNFTokenID.ts diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index ff0e2da1..14d4e96c 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -6,6 +6,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### Added * `federator_info` RPC support * Helper method for creating a cross-chain payment to/from a sidechain +* Helper method for parsing an NFTokenID ### Fixed * Type of TrustSet transaction edited, specifically LimitAmount property type (fixed typescript issue) diff --git a/packages/xrpl/src/utils/index.ts b/packages/xrpl/src/utils/index.ts index 781821e8..675d79b4 100644 --- a/packages/xrpl/src/utils/index.ts +++ b/packages/xrpl/src/utils/index.ts @@ -38,6 +38,7 @@ import { hashEscrow, hashPaymentChannel, } from './hashes' +import parseNFTokenID from './parseNFTokenID' import { percentToTransferRate, decimalToTransferRate, @@ -215,4 +216,5 @@ export { encodeForSigning, encodeForSigningClaim, createCrossChainPayment, + parseNFTokenID, } diff --git a/packages/xrpl/src/utils/parseNFTokenID.ts b/packages/xrpl/src/utils/parseNFTokenID.ts new file mode 100644 index 00000000..9fb8eec5 --- /dev/null +++ b/packages/xrpl/src/utils/parseNFTokenID.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers -- Doing hex string parsing. */ +import BigNumber from 'bignumber.js' +import { encodeAccountID } from 'ripple-address-codec' + +import { XrplError } from '../errors' + +/** + * An issuer may issue several NFTs with the same taxon; to ensure that NFTs are + * spread across multiple pages we lightly mix the taxon up by using the sequence + * (which is not under the issuer's direct control) as the seed for a simple linear + * congruential generator. + * + * From the Hull-Dobell theorem we know that f(x)=(m*x+c) mod n will yield a + * permutation of [0, n) when n is a power of 2 if m is congruent to 1 mod 4 and + * c is odd. By doing a bitwise XOR with this permutation we can scramble/unscramble + * the taxon. + * + * The XLS-20d proposal fixes m = 384160001 and c = 2459. + * We then take the modulus of 2^32 which is 4294967296. + * + * @param taxon - The scrambled or unscrambled taxon (The XOR is both the encoding and decoding) + * @param tokenSeq - The account sequence when the token was minted. Used as a psuedorandom seed. + * @returns the opposite taxon. If the taxon was scrambled it becomes unscrambled, and vice versa. + */ +function unscrambleTaxon(taxon: number, tokenSeq: number): number { + /* eslint-disable no-bitwise -- XOR is part of the encode/decode scheme. */ + return (taxon ^ (384160001 * tokenSeq + 2459)) % 4294967296 + /* eslint-enable no-bitwise */ +} + +/** + * Parses an NFTokenID into the information it is encoding. + * + * Example decoding: + * + * 000B 0539 C35B55AA096BA6D87A6E6C965A6534150DC56E5E 12C5D09E 0000000C + * +--- +--- +--------------------------------------- +------- +------- + * | | | | | + * | | | | `---> Sequence: 12 + * | | | | + * | | | `---> Scrambled Taxon: 314,953,886 + * | | | Unscrambled Taxon: 1337 + * | | | + * | | `---> Issuer: rJoxBSzpXhPtAuqFmqxQtGKjA13jUJWthE + * | | + * | `---> TransferFee: 1337.0 bps or 13.37% + * | + * `---> Flags: 11 -> lsfBurnable, lsfOnlyXRP and lsfTransferable + * + * @param tokenID - A hex string which identifies an NFToken on the ledger. + * @throws XrplError when given an invalid tokenID. + * @returns a decoded tokenID with all fields encoded within. + */ +export default function parseNFTokenID(tokenID: string): { + TokenID: string + Flags: number + TransferFee: number + Issuer: string + Taxon: number + Sequence: number +} { + const expectedLength = 64 + if (tokenID.length !== expectedLength) { + throw new XrplError(`Attempting to parse a tokenID with length ${tokenID.length} + , but expected a token with length ${expectedLength}`) + } + + const scrambledTaxon = new BigNumber(tokenID.substring(48, 56), 16).toNumber() + const sequence = new BigNumber(tokenID.substring(56, 64), 16).toNumber() + + const NFTokenIDData = { + TokenID: tokenID, + Flags: new BigNumber(tokenID.substring(0, 4), 16).toNumber(), + TransferFee: new BigNumber(tokenID.substring(4, 8), 16).toNumber(), + Issuer: encodeAccountID(Buffer.from(tokenID.substring(8, 48), 'hex')), + Taxon: unscrambleTaxon(scrambledTaxon, sequence), + Sequence: sequence, + } + + return NFTokenIDData +} diff --git a/packages/xrpl/test/utils/parseNFTokenID.ts b/packages/xrpl/test/utils/parseNFTokenID.ts new file mode 100644 index 00000000..14dc35ac --- /dev/null +++ b/packages/xrpl/test/utils/parseNFTokenID.ts @@ -0,0 +1,27 @@ +import { assert } from 'chai' +import { parseNFTokenID } from 'xrpl-local' + +import { assertResultMatch } from '../testUtils' + +describe('parseNFTokenID', function () { + it('decode a valid NFTokenID', function () { + const tokenID = + '000B0539C35B55AA096BA6D87A6E6C965A6534150DC56E5E12C5D09E0000000C' + const result = parseNFTokenID(tokenID) + const expected = { + TokenID: tokenID, + Flags: 11, + TransferFee: 1337, + Issuer: 'rJoxBSzpXhPtAuqFmqxQtGKjA13jUJWthE', + Taxon: 1337, + Sequence: 12, + } + assertResultMatch(result, expected) + }) + + it('fail when given invalid NFTokenID', function () { + assert.throws(() => { + parseNFTokenID('ABCD') + }) + }) +})