262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
import { derive, sign } from 'xrpl-accountlib'
|
|
import toast from 'react-hot-toast'
|
|
|
|
import state, { IAccount } from '../index'
|
|
import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
|
|
import { Link } from '../../components'
|
|
import { ref } from 'valtio'
|
|
import estimateFee from '../../utils/estimateFee'
|
|
import { SetHookData, toHex } from '../../utils/setHook'
|
|
import ResultLink from '../../components/ResultLink'
|
|
import { xrplSend } from './xrpl-client'
|
|
|
|
export const sha256 = async (string: string) => {
|
|
const utf8 = new TextEncoder().encode(string)
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
const hashHex = hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
|
|
return hashHex
|
|
}
|
|
|
|
|
|
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
|
if (!arrayBuffer) {
|
|
return ''
|
|
}
|
|
if (
|
|
typeof arrayBuffer !== 'object' ||
|
|
arrayBuffer === null ||
|
|
typeof arrayBuffer.byteLength !== 'number'
|
|
) {
|
|
throw new TypeError('Expected input to be an ArrayBuffer')
|
|
}
|
|
|
|
var view = new Uint8Array(arrayBuffer)
|
|
var result = ''
|
|
var value
|
|
|
|
for (var i = 0; i < view.length; i++) {
|
|
value = view[i].toString(16)
|
|
result += value.length === 1 ? '0' + value : value
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export const prepareDeployHookTx = async (
|
|
account: IAccount & { name?: string },
|
|
data: SetHookData
|
|
) => {
|
|
const activeFile = state.files[state.active]?.compiledContent
|
|
? state.files[state.active]
|
|
: state.files.filter(file => file.compiledContent)[0]
|
|
|
|
if (!state.files || state.files.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (!activeFile?.compiledContent) {
|
|
return
|
|
}
|
|
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
|
|
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
|
|
const { HookParameters } = data
|
|
const filteredHookParameters = HookParameters.filter(
|
|
hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
|
)?.map(aa => ({
|
|
HookParameter: {
|
|
HookParameterName: toHex(aa.HookParameter.HookParameterName || ''),
|
|
HookParameterValue: aa.HookParameter.HookParameterValue || ''
|
|
}
|
|
}))
|
|
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
|
|
// return {
|
|
// HookGrant: {
|
|
// ...(hg.HookGrant.Authorize && { Authorize: hg.HookGrant.Authorize }),
|
|
// // HookHash: hg.HookGrant.HookHash || undefined
|
|
// ...(hg.HookGrant.HookHash && { HookHash: hg.HookGrant.HookHash })
|
|
// }
|
|
// }
|
|
// });
|
|
if (typeof window === 'undefined') return
|
|
const tx = {
|
|
Account: account.address,
|
|
TransactionType: 'SetHook',
|
|
Sequence: account.sequence,
|
|
Fee: data.Fee,
|
|
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
|
|
Hooks: [
|
|
{
|
|
Hook: {
|
|
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
|
|
HookOn: calculateHookOn(hookOnValues),
|
|
HookNamespace,
|
|
HookApiVersion: 0,
|
|
Flags: 1,
|
|
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
|
|
...(filteredHookParameters.length > 0 && {
|
|
HookParameters: filteredHookParameters
|
|
})
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return tx
|
|
}
|
|
|
|
/*
|
|
* Turns the wasm binary into hex string, signs the transaction and deploys it to Hooks testnet.
|
|
*/
|
|
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
|
|
const activeFile = state.files[state.active]?.compiledContent
|
|
? state.files[state.active]
|
|
: state.files.filter(file => file.compiledContent)[0]
|
|
state.deployValues[activeFile.name] = data
|
|
|
|
const tx = await prepareDeployHookTx(account, data)
|
|
if (!tx) {
|
|
return
|
|
}
|
|
const keypair = derive.familySeed(account.secret)
|
|
const { signedTransaction } = sign(tx, keypair)
|
|
|
|
const currentAccount = state.accounts.find(acc => acc.address === account.address)
|
|
if (currentAccount) {
|
|
currentAccount.isLoading = true
|
|
}
|
|
|
|
let submitRes
|
|
try {
|
|
submitRes = await xrplSend({
|
|
command: 'submit',
|
|
tx_blob: signedTransaction
|
|
})
|
|
|
|
const txHash = submitRes.tx_json?.hash
|
|
const resultMsg = ref(
|
|
<>
|
|
[<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
|
|
{txHash && (
|
|
<>
|
|
Transaction hash:{' '}
|
|
<Link
|
|
as="a"
|
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{txHash}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
if (submitRes.engine_result === 'tesSUCCESS') {
|
|
state.deployLogs.push({
|
|
type: 'success',
|
|
message: 'Hook deployed successfully ✅'
|
|
})
|
|
state.deployLogs.push({
|
|
type: 'success',
|
|
message: resultMsg
|
|
})
|
|
} else if (submitRes.engine_result) {
|
|
state.deployLogs.push({
|
|
type: 'error',
|
|
message: resultMsg
|
|
})
|
|
} else {
|
|
state.deployLogs.push({
|
|
type: 'error',
|
|
message: `[${submitRes.error}] ${submitRes.error_exception}`
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
state.deployLogs.push({
|
|
type: 'error',
|
|
message: 'Error occurred while deploying'
|
|
})
|
|
}
|
|
if (currentAccount) {
|
|
currentAccount.isLoading = false
|
|
}
|
|
return submitRes
|
|
}
|
|
|
|
export const deleteHook = async (account: IAccount & { name?: string }) => {
|
|
const currentAccount = state.accounts.find(acc => acc.address === account.address)
|
|
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
|
return
|
|
}
|
|
const tx = {
|
|
Account: account.address,
|
|
TransactionType: 'SetHook',
|
|
Sequence: account.sequence,
|
|
Fee: '100000',
|
|
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
|
|
Hooks: [
|
|
{
|
|
Hook: {
|
|
CreateCode: '',
|
|
Flags: 1
|
|
}
|
|
}
|
|
]
|
|
}
|
|
const keypair = derive.familySeed(account.secret)
|
|
try {
|
|
// Update tx Fee value with network estimation
|
|
const res = await estimateFee(tx, account)
|
|
tx['Fee'] = res?.base_fee || '1000'
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
const { signedTransaction } = sign(tx, keypair)
|
|
if (currentAccount) {
|
|
currentAccount.isLoading = true
|
|
}
|
|
let submitRes
|
|
const toastId = toast.loading('Deleting hook...')
|
|
try {
|
|
submitRes = await xrplSend({
|
|
command: 'submit',
|
|
tx_blob: signedTransaction
|
|
})
|
|
|
|
if (submitRes.engine_result === 'tesSUCCESS') {
|
|
toast.success('Hook deleted successfully ✅', { id: toastId })
|
|
state.deployLogs.push({
|
|
type: 'success',
|
|
message: 'Hook deleted successfully ✅'
|
|
})
|
|
state.deployLogs.push({
|
|
type: 'success',
|
|
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
|
|
})
|
|
currentAccount.hooks = []
|
|
} else {
|
|
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
|
|
id: toastId
|
|
})
|
|
state.deployLogs.push({
|
|
type: 'error',
|
|
message: `[${submitRes.engine_result || submitRes.error}] ${
|
|
submitRes.engine_result_message || submitRes.error_exception
|
|
}`
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.log(err)
|
|
toast.error('Error occurred while deleting hook', { id: toastId })
|
|
state.deployLogs.push({
|
|
type: 'error',
|
|
message: 'Error occurred while deleting hook'
|
|
})
|
|
}
|
|
if (currentAccount) {
|
|
currentAccount.isLoading = false
|
|
}
|
|
return submitRes
|
|
}
|