Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add AMMClawback support #2893

Merged
merged 21 commits into from
Feb 12, 2025
Merged
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
@@ -180,6 +180,7 @@ fixXChainRewardRounding
fixPreviousTxnID
fixAMMv1_1
# 2.3.0 Amendments
AMMClawback
fixAMMv1_2
Credentials
NFTokenMintOffer
3 changes: 3 additions & 0 deletions packages/ripple-binary-codec/HISTORY.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

## Unreleased

### Added
* Support for the AMMClawback amendment (XLS-73)

## 2.2.0 (2024-12-23)

### Added
1 change: 1 addition & 0 deletions packages/ripple-binary-codec/src/enums/definitions.json
Original file line number Diff line number Diff line change
@@ -3073,6 +3073,7 @@
},
"TRANSACTION_TYPES": {
"AMMBid": 39,
"AMMClawback": 31,
"AMMCreate": 35,
"AMMDelete": 40,
"AMMDeposit": 36,
1 change: 1 addition & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## Unreleased

### Added
* Support for the AMMClawback amendment (XLS-73)
* Adds utility function `convertTxFlagsToNumber`
* Implementation of XLS-80d PermissionedDomain feature.
* Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
118 changes: 118 additions & 0 deletions packages/xrpl/src/models/transactions/AMMClawback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { ValidationError } from '../../errors'
import { Currency, IssuedCurrency, IssuedCurrencyAmount } from '../common'

import {
Account,
BaseTransaction,
GlobalFlags,
isAccount,
isAmount,
isCurrency,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'

/**
* Enum representing values for AMMClawback Transaction Flags.
*
* @category Transaction Flags
*/
export enum AMMClawbackFlags {
tfClawTwoAssets = 0x00000001,
}

/**
* Map of flags to boolean values representing {@link AMMClawback} transaction
* flags.
*
* @category Transaction Flags
*/
export interface AMMClawbackFlagsInterface extends GlobalFlags {
tfClawTwoAssets?: boolean
}

/**
* Claw back tokens from a holder that has deposited your issued tokens into an AMM pool.
*
* Clawback is disabled by default. To use clawback, you must send an AccountSet transaction to enable the
* Allow Trust Line Clawback setting. An issuer with any existing tokens cannot enable clawback. You can
* only enable Allow Trust Line Clawback if you have a completely empty owner directory, meaning you must
* do so before you set up any trust lines, offers, escrows, payment channels, checks, or signer lists.
* After you enable clawback, it cannot reverted: the account permanently gains the ability to claw back
* issued assets on trust lines.
*/
export interface AMMClawback extends BaseTransaction {
TransactionType: 'AMMClawback'

/**
* The account holding the asset to be clawed back.
*/
Holder: Account

/**
* Specifies the asset that the issuer wants to claw back from the AMM pool.
* In JSON, this is an object with currency and issuer fields. The issuer field must match with Account.
*/
Asset: IssuedCurrency

/**
* Specifies the other asset in the AMM's pool. In JSON, this is an object with currency and
* issuer fields (omit issuer for XRP).
*/
Asset2: Currency

/**
* The maximum amount to claw back from the AMM account. The currency and issuer subfields should match
* the Asset subfields. If this field isn't specified, or the value subfield exceeds the holder's available
* tokens in the AMM, all of the holder's tokens will be clawed back.
*/
Amount?: IssuedCurrencyAmount
}

/**
* Verify the form and type of an AMMClawback at runtime.
*
* @param tx - An AMMClawback Transaction.
* @throws When the AMMClawback is Malformed.
*/
export function validateAMMClawback(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- required
const txAMMClawback = tx as unknown as AMMClawback

validateRequiredField(tx, 'Holder', isAccount)

validateRequiredField(tx, 'Asset', isCurrency)

if (txAMMClawback.Holder === txAMMClawback.Asset.issuer) {
throw new ValidationError(
'AMMClawback: Holder and Asset.issuer must be distinct',
)
}

if (txAMMClawback.Account !== txAMMClawback.Asset.issuer) {
throw new ValidationError(
'AMMClawback: Account must be the same as Asset.issuer',
)
}

validateRequiredField(tx, 'Asset2', isCurrency)

validateOptionalField(tx, 'Amount', isAmount)

if (txAMMClawback.Amount != null) {
if (txAMMClawback.Amount.currency !== txAMMClawback.Asset.currency) {
throw new ValidationError(
'AMMClawback: currency for both Amount and Asset must be the same',
)
}

if (txAMMClawback.Amount.issuer !== txAMMClawback.Asset.issuer) {
throw new ValidationError(
'AMMClawback: issuer must be identical for both Amount and Asset',
)
}
}
}
7 changes: 6 additions & 1 deletion packages/xrpl/src/models/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -15,13 +15,18 @@ export {
} from './accountSet'
export { AccountDelete } from './accountDelete'
export { AMMBid } from './AMMBid'
export {
AMMClawbackFlags,
AMMClawbackFlagsInterface,
AMMClawback,
} from './AMMClawback'
export { AMMCreate } from './AMMCreate'
export { AMMDelete } from './AMMDelete'
export {
AMMDepositFlags,
AMMDepositFlagsInterface,
AMMDeposit,
} from './AMMDeposit'
export { AMMCreate } from './AMMCreate'
export { AMMVote } from './AMMVote'
export {
AMMWithdrawFlags,
6 changes: 6 additions & 0 deletions packages/xrpl/src/models/transactions/transaction.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { convertTxFlagsToNumber } from '../utils/flags'
import { AccountDelete, validateAccountDelete } from './accountDelete'
import { AccountSet, validateAccountSet } from './accountSet'
import { AMMBid, validateAMMBid } from './AMMBid'
import { AMMClawback, validateAMMClawback } from './AMMClawback'
import { AMMCreate, validateAMMCreate } from './AMMCreate'
import { AMMDelete, validateAMMDelete } from './AMMDelete'
import { AMMDeposit, validateAMMDeposit } from './AMMDeposit'
@@ -123,6 +124,7 @@ import {
*/
export type SubmittableTransaction =
| AMMBid
| AMMClawback
| AMMCreate
| AMMDelete
| AMMDeposit
@@ -273,6 +275,10 @@ export function validate(transaction: Record<string, unknown>): void {
validateAMMBid(tx)
break

case 'AMMClawback':
validateAMMClawback(tx)
break

case 'AMMCreate':
validateAMMCreate(tx)
break
2 changes: 2 additions & 0 deletions packages/xrpl/src/models/utils/flags.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import {
AccountRootFlags,
} from '../ledger/AccountRoot'
import { AccountSetTfFlags } from '../transactions/accountSet'
import { AMMClawbackFlags } from '../transactions/AMMClawback'
import { AMMDepositFlags } from '../transactions/AMMDeposit'
import { AMMWithdrawFlags } from '../transactions/AMMWithdraw'
import { MPTokenAuthorizeFlags } from '../transactions/MPTokenAuthorize'
@@ -47,6 +48,7 @@ export function parseAccountRootFlags(

const txToFlag = {
AccountSet: AccountSetTfFlags,
AMMClawback: AMMClawbackFlags,
AMMDeposit: AMMDepositFlags,
AMMWithdraw: AMMWithdrawFlags,
MPTokenAuthorize: MPTokenAuthorizeFlags,
57 changes: 57 additions & 0 deletions packages/xrpl/test/integration/transactions/ammClawback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AMMClawback, AMMDeposit, AMMDepositFlags, XRP } from 'xrpl'

import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { createAMMPool, testTransaction } from '../utils'

describe('AMMClawback', function () {
let testContext: XrplIntegrationTestContext

beforeAll(async () => {
testContext = await setupClient(serverUrl)
})
afterAll(async () => teardownClient(testContext))

it('base', async function () {
const ammPool = await createAMMPool(testContext.client, true)
const { issuerWallet } = ammPool
const holderWallet = ammPool.lpWallet

const asset = {
currency: 'USD',
issuer: issuerWallet.classicAddress,
}
const asset2 = {
currency: 'XRP',
} as XRP

const ammDepositTx: AMMDeposit = {
TransactionType: 'AMMDeposit',
Account: holderWallet.classicAddress,
Asset: asset,
Asset2: asset2,
Amount: {
currency: 'USD',
issuer: issuerWallet.address,
value: '10',
},
Flags: AMMDepositFlags.tfSingleAsset,
}

await testTransaction(testContext.client, ammDepositTx, holderWallet)

const ammClawback: AMMClawback = {
TransactionType: 'AMMClawback',
Account: issuerWallet.address,
Holder: holderWallet.address,
Asset: asset,
Asset2: asset2,
}

await testTransaction(testContext.client, ammClawback, issuerWallet)
})
})
15 changes: 14 additions & 1 deletion packages/xrpl/test/integration/utils.ts
Original file line number Diff line number Diff line change
@@ -373,7 +373,10 @@ export async function getIOUBalance(
return (await client.request(request)).result.lines[0].balance
}

export async function createAMMPool(client: Client): Promise<{
export async function createAMMPool(
client: Client,
enableAMMClawback = false,
): Promise<{
issuerWallet: Wallet
lpWallet: Wallet
asset: Currency
@@ -391,6 +394,16 @@ export async function createAMMPool(client: Client): Promise<{

await testTransaction(client, accountSetTx, issuerWallet)

if (enableAMMClawback) {
const accountSetTx2: AccountSet = {
TransactionType: 'AccountSet',
Account: issuerWallet.classicAddress,
SetFlag: AccountSetAsfFlags.asfAllowTrustLineClawback,
}

await testTransaction(client, accountSetTx2, issuerWallet)
}

const trustSetTx: TrustSet = {
TransactionType: 'TrustSet',
Flags: TrustSetFlags.tfClearNoRipple,
177 changes: 177 additions & 0 deletions packages/xrpl/test/models/AMMClawback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { assert } from 'chai'

import { validate, ValidationError } from '../../src'
import {
AMMClawbackFlags,
validateAMMClawback,
} from '../../src/models/transactions/AMMClawback'

/**
* AMMClawback Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('AMMClawback', function () {
let ammClawback

beforeEach(function () {
ammClawback = {
TransactionType: 'AMMClawback',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9',
Asset: {
currency: 'USD',
issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
},
Asset2: {
currency: 'XRP',
},
Amount: {
currency: 'USD',
issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
value: '1000',
},
Sequence: 1337,
} as any
})

it(`verifies valid AMMClawback`, function () {
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})

it(`verifies valid AMMClawback without Amount`, function () {
delete ammClawback.Amount
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})

it(`verifies valid AMMClawback with tfClawTwoAssets`, function () {
ammClawback.flags = AMMClawbackFlags.tfClawTwoAssets
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})

it(`throws w/ missing Holder`, function () {
delete ammClawback.Holder
const errorMessage = 'AMMClawback: missing field Holder'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ invalid field Holder`, function () {
ammClawback.Holder = 1234
const errorMessage = 'AMMClawback: invalid field Holder'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ Holder and Asset.issuer must be distinct`, function () {
ammClawback.Holder = ammClawback.Asset.issuer
const errorMessage = 'AMMClawback: Holder and Asset.issuer must be distinct'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ missing Asset`, function () {
delete ammClawback.Asset
const errorMessage = 'AMMClawback: missing field Asset'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ invalid field Asset`, function () {
ammClawback.Asset = '1000'
const errorMessage = 'AMMClawback: invalid field Asset'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ Account must be the same as Asset.issuer`, function () {
ammClawback.Account = 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn'
const errorMessage = 'AMMClawback: Account must be the same as Asset.issuer'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ missing Asset2`, function () {
delete ammClawback.Asset2
const errorMessage = 'AMMClawback: missing field Asset2'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ invalid field Asset2`, function () {
ammClawback.Asset2 = '1000'
const errorMessage = 'AMMClawback: invalid field Asset2'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ invalid field Amount`, function () {
ammClawback.Amount = 1000
const errorMessage = 'AMMClawback: invalid field Amount'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ Account must be the same as Asset.issuer`, function () {
ammClawback.Amount.currency = 'ETH'
const errorMessage =
'AMMClawback: currency for both Amount and Asset must be the same'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})

it(`throws w/ issuer must be identical for both Amount and Asset`, function () {
ammClawback.Amount.issuer = 'rnYgaEtpqpNRt3wxE39demVpDAA817rQEY'
const errorMessage =
'AMMClawback: issuer must be identical for both Amount and Asset'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
})