mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-08 14:55:49 +00:00
feat: add Clawback amendment support (#2353)
* Add Clawback transaction * Account flag lsfAllowTrustLineClawback * Support bitwise flag checking of 64 bit flags
This commit is contained in:
@@ -164,3 +164,6 @@ F1ED6B4A411D8B872E65B9DCB4C8B100375B0DD3D62D07192E011D6D7F339013 fixTrustLinesTo
|
||||
2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0 fixUniversalNumber
|
||||
75A7E01C505DD5A179DFE3E000A9B6F1EDDEB55A12F95579A23E15B15DC8BE5A ImmediateOfferKilled
|
||||
93E516234E35E08CA689FA33A6D38E103881F8DCB53023F728C307AA89D515A7 XRPFees
|
||||
# 1.12.0-b1 Amendments
|
||||
56B241D7A43D40354D02A9DC4C8DF5C7A1F930D92A9035C4E12291B3CA3E1C2B featureClawback
|
||||
27CD95EE8E1E5A537FF2F89B6CEB7C622E78E9374EBD7DCBEDFAE21CD6F16E0A fixReducedOffersV1
|
||||
|
||||
@@ -2343,6 +2343,7 @@
|
||||
"NFTokenCreateOffer": 27,
|
||||
"NFTokenCancelOffer": 28,
|
||||
"NFTokenAcceptOffer": 29,
|
||||
"Clawback": 30,
|
||||
"EnableAmendment": 100,
|
||||
"SetFee": 101,
|
||||
"UNLModify": 102
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
49
packages/xrpl/src/models/transactions/clawback.ts
Normal file
49
packages/xrpl/src/models/transactions/clawback.ts
Normal file
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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<string, unknown>): void {
|
||||
validateCheckCreate(tx)
|
||||
break
|
||||
|
||||
case 'Clawback':
|
||||
validateClawback(tx)
|
||||
break
|
||||
|
||||
case 'DepositPreauth':
|
||||
validateDepositPreauth(tx)
|
||||
break
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
115
packages/xrpl/test/integration/transactions/clawback.test.ts
Normal file
115
packages/xrpl/test/integration/transactions/clawback.test.ts
Normal file
@@ -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
packages/xrpl/test/models/clawback.test.ts
Normal file
81
packages/xrpl/test/models/clawback.test.ts
Normal file
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user