update hook

update hook

add test and validation

add estimate fee func

fix estimated fee error

add hook utils
This commit is contained in:
Denis Angell
2023-02-23 03:55:30 -05:00
parent 6d6b5a5863
commit 3a2a0918f8
11 changed files with 395 additions and 23 deletions

View File

@@ -7,6 +7,8 @@ export * from './models'
export * from './utils' export * from './utils'
export * from './sugar'
export * from './errors' export * from './errors'
export { default as Wallet } from './Wallet' export { default as Wallet } from './Wallet'

View File

@@ -60,11 +60,11 @@ export interface HookGrant {
*/ */
HookGrant: { HookGrant: {
/** /**
* * The hook hash of the grant.
*/ */
HookHash: string HookHash: string
/** /**
* * The account authorized on the grant.
*/ */
Authorize?: string Authorize?: string
} }
@@ -79,13 +79,13 @@ export interface HookParameter {
*/ */
HookParameter: { HookParameter: {
/** /**
* * The name of the parameter.
*/ */
HookParameterName: string HookParameterName: string
/** /**
* * The value of the parameter.
*/ */
HookParameterValue: number HookParameterValue: string
} }
} }
@@ -98,31 +98,31 @@ export interface Hook {
*/ */
Hook: { Hook: {
/** /**
* * The code that is executed when the hook is triggered.
*/ */
CreateCode: string CreateCode: string
/** /**
* * The flags that are set on the hook.
*/ */
Flags: number Flags: number
/** /**
* * The transactions that triggers the hook. Represented as a 256Hash
*/ */
HookOn?: string HookOn?: string
/** /**
* * The namespace of the hook.
*/ */
HookNamespace?: string HookNamespace?: string
/** /**
* * The API version of the hook.
*/ */
HookApiVersion?: number HookApiVersion?: number
/** /**
* * The parameters of the hook.
*/ */
HookParameters?: HookParameter[] HookParameters?: HookParameter[]
/** /**
* * The grants of the hook.
*/ */
HookGrants?: HookGrant[] HookGrants?: HookGrant[]
} }

View File

@@ -17,6 +17,7 @@ export interface SetHook extends BaseTransaction {
} }
const MAX_HOOKS = 4 const MAX_HOOKS = 4
const HEX_REGEX = /^[0-9A-Fa-f]{64}$/u
/** /**
* Verify the form and type of an SetHook at runtime. * Verify the form and type of an SetHook at runtime.
@@ -27,21 +28,29 @@ const MAX_HOOKS = 4
export function validateSetHook(tx: Record<string, unknown>): void { export function validateSetHook(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if (tx.Hooks === undefined) {
throw new ValidationError('SetHook: missing field Hooks')
}
if (!Array.isArray(tx.Hooks)) { if (!Array.isArray(tx.Hooks)) {
throw new ValidationError('SetHook: invalid Hooks') throw new ValidationError('SetHook: invalid Hooks')
} }
if (tx.Hooks.length === 0) {
throw new ValidationError('SetHook: need at least 1 hook in Hooks')
}
if (tx.Hooks.length > MAX_HOOKS) { if (tx.Hooks.length > MAX_HOOKS) {
throw new ValidationError( throw new ValidationError(
`SetHook: maximum of ${MAX_HOOKS} hooks allowed in Hooks`, `SetHook: maximum of ${MAX_HOOKS} hooks allowed in Hooks`,
) )
} }
for (const hook of tx.Hooks) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be a Hook
const hookObject = hook as Hook
const { HookOn, HookNamespace } = hookObject.Hook
if (HookOn !== undefined && !HEX_REGEX.test(HookOn)) {
throw new ValidationError(
`SetHook: HookOn in Hook must be a 256-bit (32-byte) hexadecimal value`,
)
}
if (HookNamespace !== undefined && !HEX_REGEX.test(HookNamespace)) {
throw new ValidationError(
`SetHook: HookNamespace in Hook must be a 256-bit (32-byte) hexadecimal value`,
)
}
}
} }

View File

@@ -8,7 +8,7 @@ import { Transaction } from '../models/transactions'
import { setTransactionFlagsToNumber } from '../models/utils/flags' import { setTransactionFlagsToNumber } from '../models/utils/flags'
import { xrpToDrops } from '../utils' import { xrpToDrops } from '../utils'
import getFeeXrp from './getFeeXrp' import { getFeeXrp } from './getFeeXrp'
// Expire unconfirmed transactions after 20 ledger versions, approximately 1 minute, by default // Expire unconfirmed transactions after 20 ledger versions, approximately 1 minute, by default
const LEDGER_OFFSET = 20 const LEDGER_OFFSET = 20

View File

@@ -14,7 +14,7 @@ const BASE_10 = 10
* @param cushion - The fee cushion to use. * @param cushion - The fee cushion to use.
* @returns The transaction fee. * @returns The transaction fee.
*/ */
export default async function getFeeXrp( export async function getFeeXrp(
client: Client, client: Client,
cushion?: number, cushion?: number,
): Promise<string> { ): Promise<string> {
@@ -43,3 +43,22 @@ export default async function getFeeXrp(
// Round fee to 6 decimal places // Round fee to 6 decimal places
return new BigNumber(fee.toFixed(NUM_DECIMAL_PLACES)).toString(BASE_10) return new BigNumber(fee.toFixed(NUM_DECIMAL_PLACES)).toString(BASE_10)
} }
/**
* Calculates the estimated transaction fee.
* Note: This is a public API that can be called directly.
*
* @param client - The Client used to connect to the ledger.
* @param txBlob - The encoded transaction to estimate the fee for.
* @returns The transaction fee.
*/
export async function getFeeEstimateXrp(
client: Client,
txBlob: string,
): Promise<string> {
const response = await client.request({
command: 'fee',
tx_blob: txBlob,
})
return response.result.drops.base_fee
}

View File

@@ -5,6 +5,7 @@ export { getBalances, getXrpBalance } from './balances'
export { default as getLedgerIndex } from './getLedgerIndex' export { default as getLedgerIndex } from './getLedgerIndex'
export { default as getOrderbook } from './getOrderbook' export { default as getOrderbook } from './getOrderbook'
export { getFeeXrp, getFeeEstimateXrp } from './getFeeXrp'
export * from './submit' export * from './submit'

View File

@@ -0,0 +1,128 @@
/**
* @module tts
* @description
* This module contains the transaction types and the function to calculate the hook on
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Required
import createHash = require('create-hash')
import { HookParameter } from '../models/common'
/**
* @constant tts
* @description
* Transaction types
*/
export const tts = {
ttPAYMENT: 0,
ttESCROW_CREATE: 1,
ttESCROW_FINISH: 2,
ttACCOUNT_SET: 3,
ttESCROW_CANCEL: 4,
ttREGULAR_KEY_SET: 5,
ttOFFER_CREATE: 7,
ttOFFER_CANCEL: 8,
ttTICKET_CREATE: 10,
ttSIGNER_LIST_SET: 12,
ttPAYCHAN_CREATE: 13,
ttPAYCHAN_FUND: 14,
ttPAYCHAN_CLAIM: 15,
ttCHECK_CREATE: 16,
ttCHECK_CASH: 17,
ttCHECK_CANCEL: 18,
ttDEPOSIT_PREAUTH: 19,
ttTRUST_SET: 20,
ttACCOUNT_DELETE: 21,
ttHOOK_SET: 22,
ttNFTOKEN_MINT: 25,
ttNFTOKEN_BURN: 26,
ttNFTOKEN_CREATE_OFFER: 27,
ttNFTOKEN_CANCEL_OFFER: 28,
ttNFTOKEN_ACCEPT_OFFER: 29,
}
/**
* @typedef TTS
* @description
* Transaction types
*/
export type TTS = typeof tts
/**
* Calculate the hook on
*
* @param arr - array of transaction types
* @returns the hook on
*/
export function calculateHookOn(arr: Array<keyof TTS>): string {
let hash = '0x3e3ff5bf'
arr.forEach((nth) => {
let value = BigInt(hash)
// eslint-disable-next-line no-bitwise -- Required
value ^= BigInt(1) << BigInt(tts[nth])
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required
hash = `0x${value.toString(16)}`
})
hash = hash.replace('0x', '')
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required
hash = hash.padStart(64, '0')
return hash.toUpperCase()
}
/**
* Calculate the sha256 of a string
*
* @param string - the string to calculate the sha256
* @returns the sha256
*/
export async function sha256(string: string): Promise<string> {
const hash = createHash('sha256')
hash.update(string)
const hashBuffer = hash.digest()
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('')
return hashHex
}
/**
* Calculate the hex of a namespace
*
* @param namespace - the namespace to calculate the hex
* @returns the hex
*/
export async function hexNamespace(namespace: string): Promise<string> {
return (await sha256(namespace)).toUpperCase()
}
/**
* Calculate the hex of the hook parameters
*
* @param data - the hook parameters
* @returns the hex of the hook parameters
*/
export function hexHookParameters(data: HookParameter[]): HookParameter[] {
const hookParameters: HookParameter[] = []
for (const parameter of data) {
hookParameters.push({
HookParameter: {
HookParameterName: Buffer.from(
parameter.HookParameter.HookParameterName,
'utf8',
)
.toString('hex')
.toUpperCase(),
HookParameterValue: Buffer.from(
parameter.HookParameter.HookParameterValue,
'utf8',
)
.toString('hex')
.toUpperCase(),
},
})
}
return hookParameters
}

View File

@@ -40,6 +40,7 @@ import {
hashEscrow, hashEscrow,
hashPaymentChannel, hashPaymentChannel,
} from './hashes' } from './hashes'
import { calculateHookOn, hexNamespace, hexHookParameters, TTS } from './hooks'
import parseNFTokenID from './parseNFTokenID' import parseNFTokenID from './parseNFTokenID'
import { import {
percentToTransferRate, percentToTransferRate,
@@ -222,4 +223,8 @@ export {
getNFTokenID, getNFTokenID,
createCrossChainPayment, createCrossChainPayment,
parseNFTokenID, parseNFTokenID,
calculateHookOn,
hexNamespace,
hexHookParameters,
TTS,
} }

View File

@@ -1,6 +1,6 @@
import { assert } from 'chai' import { assert } from 'chai'
import getFeeXrp from '../../src/sugar/getFeeXrp' import { getFeeXrp } from '../../src/sugar/getFeeXrp'
import rippled from '../fixtures/rippled' import rippled from '../fixtures/rippled'
import { import {
setupClient, setupClient,

View File

@@ -0,0 +1,150 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateSetHook } from '../../src/models/transactions/setHook'
/**
* SetHook Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('SetHook', function () {
let setHookTx
beforeEach(function () {
setHookTx = {
Flags: 0,
TransactionType: 'SetHook',
Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
Fee: '12',
Hooks: [
{
Hook: {
CreateCode:
'0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22',
HookOn:
'000000000000000000000000000000000000000000000000000000003E3FF5B7',
Flags: 1,
HookApiVersion: 0,
HookNamespace:
'4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9',
},
},
],
} as any
})
it(`verifies valid SetHook`, function () {
assert.doesNotThrow(() => validateSetHook(setHookTx))
assert.doesNotThrow(() => validate(setHookTx))
})
// it(`throws w/ empty Hooks`, function () {
// setHookTx.Hooks = []
// assert.throws(
// () => validateSetHook(setHookTx),
// ValidationError,
// 'SetHook: need at least 1 member in Hooks',
// )
// assert.throws(
// () => validate(setHookTx),
// ValidationError,
// 'SetHook: need at least 1 member in Hooks',
// )
// })
it(`throws w/ invalid Hooks`, function () {
setHookTx.Hooks = 'khgfgyhujk'
assert.throws(
() => validateSetHook(setHookTx),
ValidationError,
'SetHook: invalid Hooks',
)
assert.throws(
() => validate(setHookTx),
ValidationError,
'SetHook: invalid Hooks',
)
})
it(`throws w/ maximum of 4 members allowed in Hooks`, function () {
setHookTx.Hooks = []
const hook = {
Hook: {
CreateCode:
'0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22',
HookOn:
'000000000000000000000000000000000000000000000000000000003E3FF5B7',
Flags: 1,
HookApiVersion: 0,
HookNamespace:
'4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9',
},
}
setHookTx.Hooks.push(hook)
setHookTx.Hooks.push(hook)
setHookTx.Hooks.push(hook)
setHookTx.Hooks.push(hook)
setHookTx.Hooks.push(hook)
const errorMessage = 'SetHook: maximum of 4 hooks allowed in Hooks'
assert.throws(
() => validateSetHook(setHookTx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(setHookTx), ValidationError, errorMessage)
})
it(`throws w/ invalid HookOn in Hooks`, function () {
setHookTx.SignerQuorum = 2
setHookTx.Hooks = [
{
Hook: {
CreateCode:
'0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22',
HookOn: '',
Flags: 1,
HookApiVersion: 0,
HookNamespace:
'4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9',
},
},
]
const errorMessage =
'SetHook: HookOn in Hook must be a 256-bit (32-byte) hexadecimal value'
assert.throws(
() => validateSetHook(setHookTx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(setHookTx), ValidationError, errorMessage)
})
it(`throws w/ invalid HookNamespace in Hooks`, function () {
setHookTx.SignerQuorum = 2
setHookTx.Hooks = [
{
Hook: {
CreateCode:
'0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22',
HookOn:
'000000000000000000000000000000000000000000000000000000003E3FF5B7',
Flags: 1,
HookApiVersion: 0,
HookNamespace: '',
},
},
]
const errorMessage =
'SetHook: HookNamespace in Hook must be a 256-bit (32-byte) hexadecimal value'
assert.throws(
() => validateSetHook(setHookTx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(setHookTx), ValidationError, errorMessage)
})
})

View File

@@ -0,0 +1,58 @@
import { assert } from 'chai'
import {
calculateHookOn,
hexNamespace,
hexHookParameters,
TTS,
} from '../../src'
describe('test hook on', function () {
it('all', function () {
const result = calculateHookOn([])
assert.equal(
result,
'000000000000000000000000000000000000000000000000000000003E3FF5BF',
)
})
it('one', function () {
const invokeOn: Array<keyof TTS> = ['ttACCOUNT_SET']
const result = calculateHookOn(invokeOn)
assert.equal(
result,
'000000000000000000000000000000000000000000000000000000003E3FF5B7',
)
})
})
describe('test hook namespace', function () {
it('basic', async function () {
const result = await hexNamespace('starter')
assert.equal(
result,
'4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9',
)
})
})
describe('test hook parameters', function () {
it('basic', async function () {
const parameters = [
{
HookParameter: {
HookParameterName: 'name1',
HookParameterValue: 'value1',
},
},
]
const result = hexHookParameters(parameters)
assert.deepEqual(result, [
{
HookParameter: {
HookParameterName: '6E616D6531',
HookParameterValue: '76616C756531',
},
},
])
})
})