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

Clawback #2353

Merged
merged 20 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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 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 currencies. Once enabled, cannot be disabled.
*/
lsfAllowClawback?: 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 currencies. Once enabled, cannot be disabled.
*/
lsfAllowClawback = 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 currencies. Once enabled, cannot be disabled.
*/
allowClawback: 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 funds */
asfAllowClawback = 16,
}

/**
Expand Down
41 changes: 41 additions & 0 deletions packages/xrpl/src/models/transactions/clawback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount } from '../common'

import { BaseTransaction, validateBaseTransaction, isAmount } 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) {
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
throw new ValidationError('Clawback: missing field Amount')
}

if (!isAmount(tx.Amount)) {
throw new ValidationError('Clawback: invalid Amount')
}
}
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
98 changes: 98 additions & 0 deletions packages/xrpl/test/integration/transactions/clawback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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.asfAllowClawback,
}
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 response1 = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'state',
})
assert.lengthOf(
response1.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: '1000',
},
}
await testTransaction(testContext.client, tx, testContext.wallet)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
},
TIMEOUT,
)
})
51 changes: 51 additions & 0 deletions packages/xrpl/test/models/clawback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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',
)
})
})
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.lsfAllowClawback

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.lsfAllowClawback,
)
})

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.lsfAllowClawback)
})
})
})