From 63ea6e8c9bee2c38647085dbf9975b434e6a28c1 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:39:48 -0400 Subject: [PATCH] feat: add Clawback amendment support (#2353) * Add Clawback transaction * Account flag lsfAllowTrustLineClawback * Support bitwise flag checking of 64 bit flags --- .ci-config/rippled.cfg | 3 + .../src/enums/definitions.json | 1 + .../test/definitions.test.js | 2 +- .../xrpl/src/models/ledger/AccountRoot.ts | 8 ++ .../xrpl/src/models/methods/accountInfo.ts | 4 + .../src/models/transactions/accountSet.ts | 2 + .../xrpl/src/models/transactions/clawback.ts | 49 ++++++++ .../xrpl/src/models/transactions/index.ts | 1 + .../src/models/transactions/transaction.ts | 6 + packages/xrpl/src/models/utils/flags.ts | 8 +- packages/xrpl/src/models/utils/index.ts | 4 +- .../integration/transactions/clawback.test.ts | 115 ++++++++++++++++++ packages/xrpl/test/models/clawback.test.ts | 81 ++++++++++++ packages/xrpl/test/models/utils.test.ts | 7 +- 14 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 packages/xrpl/src/models/transactions/clawback.ts create mode 100644 packages/xrpl/test/integration/transactions/clawback.test.ts create mode 100644 packages/xrpl/test/models/clawback.test.ts diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index ab4418963a..41b6a97f53 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -164,3 +164,6 @@ F1ED6B4A411D8B872E65B9DCB4C8B100375B0DD3D62D07192E011D6D7F339013 fixTrustLinesTo 2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0 fixUniversalNumber 75A7E01C505DD5A179DFE3E000A9B6F1EDDEB55A12F95579A23E15B15DC8BE5A ImmediateOfferKilled 93E516234E35E08CA689FA33A6D38E103881F8DCB53023F728C307AA89D515A7 XRPFees +# 1.12.0-b1 Amendments +56B241D7A43D40354D02A9DC4C8DF5C7A1F930D92A9035C4E12291B3CA3E1C2B featureClawback +27CD95EE8E1E5A537FF2F89B6CEB7C622E78E9374EBD7DCBEDFAE21CD6F16E0A fixReducedOffersV1 diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index a59de992eb..19ab2820be 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -2343,6 +2343,7 @@ "NFTokenCreateOffer": 27, "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, + "Clawback": 30, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/packages/ripple-binary-codec/test/definitions.test.js b/packages/ripple-binary-codec/test/definitions.test.js index 390bec4166..5f6bb17fe4 100644 --- a/packages/ripple-binary-codec/test/definitions.test.js +++ b/packages/ripple-binary-codec/test/definitions.test.js @@ -21,7 +21,7 @@ describe('encode and decode using new types as a parameter', function () { // Normally this would be generated directly from rippled with something like `server_definitions`. // Added here to make it easier to see what is actually changing in the definitions.json file. const definitions = JSON.parse(JSON.stringify(normalDefinitionsJson)) - definitions.TRANSACTION_TYPES['NewTestTransaction'] = 30 + definitions.TRANSACTION_TYPES['NewTestTransaction'] = 75 const newDefs = new XrplDefinitions(definitions) diff --git a/packages/xrpl/src/models/ledger/AccountRoot.ts b/packages/xrpl/src/models/ledger/AccountRoot.ts index 359bfee05f..87bde46815 100644 --- a/packages/xrpl/src/models/ledger/AccountRoot.ts +++ b/packages/xrpl/src/models/ledger/AccountRoot.ts @@ -142,6 +142,10 @@ export interface AccountRootFlagsInterface { * Disallow incoming Trustlines from other accounts. */ lsfDisallowIncomingTrustline?: boolean + /** + * This address can claw back issued IOUs. Once enabled, cannot be disabled. + */ + lsfAllowTrustLineClawback?: boolean } export enum AccountRootFlags { @@ -198,4 +202,8 @@ export enum AccountRootFlags { * Disallow incoming Trustlines from other accounts. */ lsfDisallowIncomingTrustline = 0x20000000, + /** + * This address can claw back issued IOUs. Once enabled, cannot be disabled. + */ + lsfAllowTrustLineClawback = 0x80000000, } diff --git a/packages/xrpl/src/models/methods/accountInfo.ts b/packages/xrpl/src/models/methods/accountInfo.ts index be4cc36ebf..d872b4a100 100644 --- a/packages/xrpl/src/models/methods/accountInfo.ts +++ b/packages/xrpl/src/models/methods/accountInfo.ts @@ -127,6 +127,10 @@ export interface AccountInfoAccountFlags { * Requires incoming payments to specify a Destination Tag. */ requireDestinationTag: boolean + /** + * This address can claw back issued IOUs. Once enabled, cannot be disabled. + */ + allowTrustLineClawback: boolean } /** diff --git a/packages/xrpl/src/models/transactions/accountSet.ts b/packages/xrpl/src/models/transactions/accountSet.ts index f90ebce592..a3916356a2 100644 --- a/packages/xrpl/src/models/transactions/accountSet.ts +++ b/packages/xrpl/src/models/transactions/accountSet.ts @@ -56,6 +56,8 @@ export enum AccountSetAsfFlags { asfDisallowIncomingPayChan = 14, /** Disallow other accounts from creating incoming Trustlines */ asfDisallowIncomingTrustline = 15, + /** Permanently gain the ability to claw back issued IOUs */ + asfAllowTrustLineClawback = 16, } /** diff --git a/packages/xrpl/src/models/transactions/clawback.ts b/packages/xrpl/src/models/transactions/clawback.ts new file mode 100644 index 0000000000..fb51859cfc --- /dev/null +++ b/packages/xrpl/src/models/transactions/clawback.ts @@ -0,0 +1,49 @@ +import { ValidationError } from '../../errors' +import { IssuedCurrencyAmount } from '../common' + +import { + BaseTransaction, + validateBaseTransaction, + isIssuedCurrency, +} from './common' + +/** + * The Clawback transaction is used by the token issuer to claw back + * issued tokens from a holder. + */ +export interface Clawback extends BaseTransaction { + TransactionType: 'Clawback' + /** + * Indicates the AccountID that submitted this transaction. The account MUST + * be the issuer of the currency. + */ + Account: string + /** + * The amount of currency to deliver, and it must be non-XRP. The nested field + * names MUST be lower-case. The `issuer` field MUST be the holder's address, + * whom to be clawed back. + */ + Amount: IssuedCurrencyAmount +} + +/** + * Verify the form and type of an Clawback at runtime. + * + * @param tx - An Clawback Transaction. + * @throws When the Clawback is Malformed. + */ +export function validateClawback(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Amount == null) { + throw new ValidationError('Clawback: missing field Amount') + } + + if (!isIssuedCurrency(tx.Amount)) { + throw new ValidationError('Clawback: invalid Amount') + } + + if (isIssuedCurrency(tx.Amount) && tx.Account === tx.Amount.issuer) { + throw new ValidationError('Clawback: invalid holder Account') + } +} diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index 4d51ab05f1..c817df3367 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -49,3 +49,4 @@ export { SignerListSet } from './signerListSet' export { TicketCreate } from './ticketCreate' export { TrustSetFlagsInterface, TrustSetFlags, TrustSet } from './trustSet' export { UNLModify } from './UNLModify' +export { Clawback } from './clawback' diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index 6e24c458b0..aee51a679e 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -11,6 +11,7 @@ import { AccountSet, validateAccountSet } from './accountSet' import { CheckCancel, validateCheckCancel } from './checkCancel' import { CheckCash, validateCheckCash } from './checkCash' import { CheckCreate, validateCheckCreate } from './checkCreate' +import { Clawback, validateClawback } from './clawback' import { isIssuedCurrency } from './common' import { DepositPreauth, validateDepositPreauth } from './depositPreauth' import { EscrowCancel, validateEscrowCancel } from './escrowCancel' @@ -60,6 +61,7 @@ export type Transaction = | CheckCancel | CheckCash | CheckCreate + | Clawback | DepositPreauth | EscrowCancel | EscrowCreate @@ -177,6 +179,10 @@ export function validate(transaction: Record): void { validateCheckCreate(tx) break + case 'Clawback': + validateClawback(tx) + break + case 'DepositPreauth': validateDepositPreauth(tx) break diff --git a/packages/xrpl/src/models/utils/flags.ts b/packages/xrpl/src/models/utils/flags.ts index f185c99b84..5f11205778 100644 --- a/packages/xrpl/src/models/utils/flags.ts +++ b/packages/xrpl/src/models/utils/flags.ts @@ -36,8 +36,12 @@ export function parseAccountRootFlags( ): AccountRootFlagsInterface { const flagsInterface: AccountRootFlagsInterface = {} - Object.keys(AccountRootFlags).forEach((flag) => { - if (isFlagEnabled(flags, AccountRootFlags[flag])) { + // If we use keys all will be strings and enums are reversed during transpilation + Object.values(AccountRootFlags).forEach((flag) => { + if ( + typeof flag === 'string' && + isFlagEnabled(flags, AccountRootFlags[flag]) + ) { flagsInterface[flag] = true } }) diff --git a/packages/xrpl/src/models/utils/index.ts b/packages/xrpl/src/models/utils/index.ts index cb79719707..5a1d7520f0 100644 --- a/packages/xrpl/src/models/utils/index.ts +++ b/packages/xrpl/src/models/utils/index.ts @@ -22,8 +22,8 @@ export function onlyHasFields( * @returns True if checkFlag is enabled within Flags. */ export function isFlagEnabled(Flags: number, checkFlag: number): boolean { - // eslint-disable-next-line no-bitwise -- flags needs bitwise - return (checkFlag & Flags) === checkFlag + // eslint-disable-next-line no-bitwise -- flags need bitwise + return (BigInt(checkFlag) & BigInt(Flags)) === BigInt(checkFlag) } /** diff --git a/packages/xrpl/test/integration/transactions/clawback.test.ts b/packages/xrpl/test/integration/transactions/clawback.test.ts new file mode 100644 index 0000000000..e0b3aedc43 --- /dev/null +++ b/packages/xrpl/test/integration/transactions/clawback.test.ts @@ -0,0 +1,115 @@ +import { assert } from 'chai' + +import { + AccountSet, + AccountSetAsfFlags, + TrustSet, + Payment, + Clawback, +} from '../../../src' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { generateFundedWallet, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('Clawback', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const wallet2 = await generateFundedWallet(testContext.client) + + const setupAccountSetTx: AccountSet = { + TransactionType: 'AccountSet', + Account: testContext.wallet.classicAddress, + SetFlag: AccountSetAsfFlags.asfAllowTrustLineClawback, + } + await testTransaction( + testContext.client, + setupAccountSetTx, + testContext.wallet, + ) + + const setupTrustSetTx: TrustSet = { + TransactionType: 'TrustSet', + Account: wallet2.classicAddress, + LimitAmount: { + currency: 'USD', + issuer: testContext.wallet.classicAddress, + value: '1000', + }, + } + await testTransaction(testContext.client, setupTrustSetTx, wallet2) + + const setupPaymentTx: Payment = { + TransactionType: 'Payment', + Account: testContext.wallet.classicAddress, + Destination: wallet2.classicAddress, + Amount: { + currency: 'USD', + issuer: testContext.wallet.classicAddress, + value: '1000', + }, + } + await testTransaction( + testContext.client, + setupPaymentTx, + testContext.wallet, + ) + + // verify that line is created + const objectsResponse = await testContext.client.request({ + command: 'account_objects', + account: wallet2.classicAddress, + type: 'state', + }) + assert.lengthOf( + objectsResponse.result.account_objects, + 1, + 'Should be exactly one line on the ledger', + ) + + // actual test - clawback + const tx: Clawback = { + TransactionType: 'Clawback', + Account: testContext.wallet.classicAddress, + Amount: { + currency: 'USD', + issuer: wallet2.classicAddress, + value: '500', + }, + } + await testTransaction(testContext.client, tx, testContext.wallet) + + // verify amount clawed back + const linesResponse = await testContext.client.request({ + command: 'account_lines', + account: wallet2.classicAddress, + }) + + assert.lengthOf( + linesResponse.result.lines, + 1, + 'Should be exactly one line on the ledger', + ) + assert.equal( + '500', + linesResponse.result.lines[0].balance, + `Holder balance incorrect after Clawback`, + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/models/clawback.test.ts b/packages/xrpl/test/models/clawback.test.ts new file mode 100644 index 0000000000..b60f765311 --- /dev/null +++ b/packages/xrpl/test/models/clawback.test.ts @@ -0,0 +1,81 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' + +/** + * Clawback Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('Clawback', function () { + it(`verifies valid Clawback`, function () { + const validClawback = { + TransactionType: 'Clawback', + Amount: { + currency: 'DSH', + issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX', + value: '43.11584856965009', + }, + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assert.doesNotThrow(() => validate(validClawback)) + }) + + it(`throws w/ missing Amount`, function () { + const missingAmount = { + TransactionType: 'Clawback', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assert.throws( + () => validate(missingAmount), + ValidationError, + 'Clawback: missing field Amount', + ) + }) + + it(`throws w/ invalid Amount`, function () { + const invalidAmount = { + TransactionType: 'Clawback', + Amount: 100000000, + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assert.throws( + () => validate(invalidAmount), + ValidationError, + 'Clawback: invalid Amount', + ) + + const invalidStrAmount = { + TransactionType: 'Clawback', + Amount: '1234', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assert.throws( + () => validate(invalidStrAmount), + ValidationError, + 'Clawback: invalid Amount', + ) + }) + + it(`throws w/ invalid holder Account`, function () { + const invalidAccount = { + TransactionType: 'Clawback', + Amount: { + currency: 'DSH', + issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + value: '43.11584856965009', + }, + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assert.throws( + () => validate(invalidAccount), + ValidationError, + 'Clawback: invalid holder Account', + ) + }) +}) diff --git a/packages/xrpl/test/models/utils.test.ts b/packages/xrpl/test/models/utils.test.ts index 932821d1df..f5d34574cc 100644 --- a/packages/xrpl/test/models/utils.test.ts +++ b/packages/xrpl/test/models/utils.test.ts @@ -166,7 +166,8 @@ describe('Models Utils', function () { AccountRootFlags.lsfDisallowIncomingNFTokenOffer | AccountRootFlags.lsfDisallowIncomingCheck | AccountRootFlags.lsfDisallowIncomingPayChan | - AccountRootFlags.lsfDisallowIncomingTrustline + AccountRootFlags.lsfDisallowIncomingTrustline | + AccountRootFlags.lsfAllowTrustLineClawback const parsed = parseAccountRootFlags(accountRootFlags) @@ -183,7 +184,8 @@ describe('Models Utils', function () { parsed.lsfDisallowIncomingNFTokenOffer && parsed.lsfDisallowIncomingCheck && parsed.lsfDisallowIncomingPayChan && - parsed.lsfDisallowIncomingTrustline, + parsed.lsfDisallowIncomingTrustline && + parsed.lsfAllowTrustLineClawback, ) }) @@ -203,6 +205,7 @@ describe('Models Utils', function () { assert.isUndefined(parsed.lsfDisallowIncomingCheck) assert.isUndefined(parsed.lsfDisallowIncomingPayChan) assert.isUndefined(parsed.lsfDisallowIncomingTrustline) + assert.isUndefined(parsed.lsfAllowTrustLineClawback) }) }) })