Skip to content

Commit

Permalink
feat: add Clawback amendment support (#2353)
Browse files Browse the repository at this point in the history
* Add Clawback transaction
* Account flag lsfAllowTrustLineClawback
* Support bitwise flag checking of 64 bit flags
  • Loading branch information
shawnxie999 authored Jul 19, 2023
1 parent c564161 commit 63ea6e8
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,6 @@ F1ED6B4A411D8B872E65B9DCB4C8B100375B0DD3D62D07192E011D6D7F339013 fixTrustLinesTo
2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0 fixUniversalNumber
75A7E01C505DD5A179DFE3E000A9B6F1EDDEB55A12F95579A23E15B15DC8BE5A ImmediateOfferKilled
93E516234E35E08CA689FA33A6D38E103881F8DCB53023F728C307AA89D515A7 XRPFees
# 1.12.0-b1 Amendments
56B241D7A43D40354D02A9DC4C8DF5C7A1F930D92A9035C4E12291B3CA3E1C2B featureClawback
27CD95EE8E1E5A537FF2F89B6CEB7C622E78E9374EBD7DCBEDFAE21CD6F16E0A fixReducedOffersV1
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 @@ -2343,6 +2343,7 @@
"NFTokenCreateOffer": 27,
"NFTokenCancelOffer": 28,
"NFTokenAcceptOffer": 29,
"Clawback": 30,
"EnableAmendment": 100,
"SetFee": 101,
"UNLModify": 102
Expand Down
2 changes: 1 addition & 1 deletion packages/ripple-binary-codec/test/definitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions packages/xrpl/src/models/ledger/AccountRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
4 changes: 4 additions & 0 deletions packages/xrpl/src/models/methods/accountInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/xrpl/src/models/transactions/accountSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/xrpl/src/models/transactions/clawback.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): 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')
}
}
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 @@ -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'
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 @@ -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'
Expand Down Expand Up @@ -60,6 +61,7 @@ export type Transaction =
| CheckCancel
| CheckCash
| CheckCreate
| Clawback
| DepositPreauth
| EscrowCancel
| EscrowCreate
Expand Down Expand Up @@ -177,6 +179,10 @@ export function validate(transaction: Record<string, unknown>): void {
validateCheckCreate(tx)
break

case 'Clawback':
validateClawback(tx)
break

case 'DepositPreauth':
validateDepositPreauth(tx)
break
Expand Down
8 changes: 6 additions & 2 deletions packages/xrpl/src/models/utils/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
Expand Down
4 changes: 2 additions & 2 deletions packages/xrpl/src/models/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
115 changes: 115 additions & 0 deletions packages/xrpl/test/integration/transactions/clawback.test.ts
Original file line number Diff line number Diff line change
@@ -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,
)
})
81 changes: 81 additions & 0 deletions packages/xrpl/test/models/clawback.test.ts
Original file line number Diff line number Diff line change
@@ -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',
)
})
})
7 changes: 5 additions & 2 deletions packages/xrpl/test/models/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ describe('Models Utils', function () {
AccountRootFlags.lsfDisallowIncomingNFTokenOffer |
AccountRootFlags.lsfDisallowIncomingCheck |
AccountRootFlags.lsfDisallowIncomingPayChan |
AccountRootFlags.lsfDisallowIncomingTrustline
AccountRootFlags.lsfDisallowIncomingTrustline |
AccountRootFlags.lsfAllowTrustLineClawback

const parsed = parseAccountRootFlags(accountRootFlags)

Expand All @@ -183,7 +184,8 @@ describe('Models Utils', function () {
parsed.lsfDisallowIncomingNFTokenOffer &&
parsed.lsfDisallowIncomingCheck &&
parsed.lsfDisallowIncomingPayChan &&
parsed.lsfDisallowIncomingTrustline,
parsed.lsfDisallowIncomingTrustline &&
parsed.lsfAllowTrustLineClawback,
)
})

Expand All @@ -203,6 +205,7 @@ describe('Models Utils', function () {
assert.isUndefined(parsed.lsfDisallowIncomingCheck)
assert.isUndefined(parsed.lsfDisallowIncomingPayChan)
assert.isUndefined(parsed.lsfDisallowIncomingTrustline)
assert.isUndefined(parsed.lsfAllowTrustLineClawback)
})
})
})

0 comments on commit 63ea6e8

Please sign in to comment.