Skip to content

Commit

Permalink
feat: add recycle sudt cells
Browse files Browse the repository at this point in the history
  • Loading branch information
Keith-CY committed Aug 16, 2024
1 parent 3fd89f1 commit cd9ae83
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 6 deletions.
9 changes: 8 additions & 1 deletion packages/neuron/public/locales/en/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
"Processing": "Processing",
"Transaction_Complete": "Transaction complete",
"Download": "Download",
"Incorrect_JSON": "JSON format is incorrect, Please check the upload file"
"Incorrect_JSON": "JSON format is incorrect, Please check the upload file",
"Holder_Address": "Address to recycle",
"Args_of_SUDT": "Args of sUDT",
"Receiver_Address": "Address to receive CKB",
"Recycle_SUDT_Cells": "Recycle sUDT Cells",
"Generate": "Generate",
"Recycle_SUDT_Cells_Tip": "Generate a transaction to wipe and merge specific sUDT cells, the transaction can be signed and submitted by Neuron => Menu => Tools => Offline Sign / Broadcast Transaction",
"Generate_TX_to_recycle_sudt_cells": "Generated a transaction to recycle {{total}} CKB from {{cellCount}} Cells"
}
9 changes: 8 additions & 1 deletion packages/neuron/public/locales/zh/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
"Processing": "处理中",
"Transaction_Complete": "转换完成",
"Download": "下载",
"Incorrect_JSON": "JSON 格式不正确,请检查上传的文件"
"Incorrect_JSON": "JSON 格式不正确,请检查上传的文件",
"Holder_Address": "待回收地址",
"Args_of_SUDT": "sUDT 的 Args",
"Receiver_Address": "接收 CKB 地址",
"Recycle_SUDT_Cells": "回收 sUDT Cells",
"Generate": "生成",
"Recycle_SUDT_Cells_Tip": "构建将清除并合并指定 sUDT Cells 的交易, 交易可以通过 Neuron 的 菜单 => 工具 => 离线签名 / 广播交易 功能签名及提交",
"Generate_TX_to_recycle_sudt_cells": "已构建交易从 {{cellCount}} Cells 回收 {{total}} CKB"
}
81 changes: 81 additions & 0 deletions packages/neuron/src/pages/tools/RecycleCells/generate_tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Indexer } from '@ckb-lumos/ckb-indexer'
import { BI, helpers, Script, config, Cell, BIish } from '@ckb-lumos/lumos'
import { sealTransaction } from '@ckb-lumos/lumos/helpers'
import { CKB_MAINNET_NODE_URL, CKB_TESTNET_NODE_URL } from '../../../utils/constants'

export async function generateTxToRecycleSUDTCells(holder: string, sudtArgs: string, receiver: string) {
const IS_MAINNET = holder.startsWith('ckb')
const ENDPOINT = IS_MAINNET ? CKB_MAINNET_NODE_URL : CKB_TESTNET_NODE_URL
const indexer = new Indexer(ENDPOINT)

const CONFIG = IS_MAINNET ? config.predefined.LINA : config.predefined.AGGRON4

const SUDT_CONFIG = CONFIG.SCRIPTS.SUDT
const SUDT_CELL_DEP = {
outPoint: { txHash: SUDT_CONFIG.TX_HASH, index: SUDT_CONFIG.INDEX },
depType: SUDT_CONFIG.DEP_TYPE,
}

const ANYONE_CAN_PAY_CONFIG = CONFIG.SCRIPTS.ANYONE_CAN_PAY
const ANYONE_CAN_PAY_DEP = {
outPoint: { txHash: ANYONE_CAN_PAY_CONFIG.TX_HASH, index: ANYONE_CAN_PAY_CONFIG.INDEX },
depType: ANYONE_CAN_PAY_CONFIG.DEP_TYPE,
}

const lock: Script = helpers.parseAddress(holder, { config: CONFIG })
const type: Script = {
codeHash: SUDT_CONFIG.CODE_HASH,
hashType: SUDT_CONFIG.HASH_TYPE,
args: sudtArgs,
}

const receiverLock = helpers.parseAddress(receiver, { config: CONFIG })

const cells: Array<Cell> = []

const collector = indexer.collector({ type, lock })

for await (const cell of collector.collect()) {
cells.push(cell)
}
if (!cells.length) {
throw new Error('No cells found')
}

const totalCKB = cells.reduce((acc, cur) => BI.from(cur.cellOutput.capacity).add(acc), BI.from(0))

let txSkeleton = helpers.TransactionSkeleton()
txSkeleton = txSkeleton.update('cellDeps', deps => deps.push(SUDT_CELL_DEP, ANYONE_CAN_PAY_DEP))

txSkeleton = txSkeleton = txSkeleton = txSkeleton.update('inputs', inputs => inputs.push(...cells))

const SIZE = 340 + 52 * cells.length // precomputed
const fee = calculateFee(SIZE)

const total = totalCKB.sub(fee).toHexString()

txSkeleton = txSkeleton = txSkeleton.update('outputs', outputs => {
const receiverCell: Cell = {
cellOutput: {
capacity: total,
lock: receiverLock,
},
data: '0x',
}
return outputs.push(receiverCell)
})

const tx = sealTransaction(txSkeleton, [])
return { tx, total }
}

function calculateFee(size: number, feeRate: BIish = 1200): BI {
const ratio = BI.from(1000)
const base = BI.from(size).mul(feeRate)
const fee = base.div(ratio)

if (fee.mul(ratio).lt(base)) {
return fee.add(1)
}
return BI.from(fee)
}
55 changes: 55 additions & 0 deletions packages/neuron/src/pages/tools/RecycleCells/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.container {
display: flex;
flex-direction: column;
gap: 16px;
align-items: start;
justify-content: center;
width: 100%;

fieldset {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
appearance: none;

label {
flex-basis: 240px;
}
}

input {
width: 100%;
height: 2em;
padding: 16px 8px;
color: #fff;
background: transparent;
border: 1px solid rgb(255 255 255 / 20%);
border-radius: 8px;
}

.overview {
height: 1rem;
}

.err {
color: #f62a2a;
font-weight: 400;
}

.loading {
width: 1rem;
height: 1rem;
border: #000 2px solid;
border-top-color: transparent;
border-radius: 50%;
animation: spin 1s infinite linear;
}

@keyframes spin {
100% {
transform: rotate(1turn);
}
}
}
90 changes: 90 additions & 0 deletions packages/neuron/src/pages/tools/RecycleCells/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useTranslation } from 'next-i18next'
import { useState } from 'react'
import { BI } from '@ckb-lumos/lumos'
import { Button } from '../../../components/Button'
import exportTxToSign from '../../../utils/export-tx-to-sign'
import { generateTxToRecycleSUDTCells } from './generate_tx'
import { downloadFile } from '../../../utils'
import styles from './index.module.scss'

export const RecycleCells = () => {
const { t } = useTranslation('tools')
const [isLoading, setIsLoading] = useState(false)
const [overview, setOverview] = useState('')
const [err, setErr] = useState('')

const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
e.stopPropagation()
e.preventDefault()
setIsLoading(true)
setOverview('')
setErr('')

const { holder: holderElm, sudt: sudtElm, receiver: receiverElm } = e.currentTarget

if (
!(holderElm instanceof HTMLInputElement) ||
!(sudtElm instanceof HTMLInputElement) ||
!(receiverElm instanceof HTMLInputElement)
) {
return
}

const handle = async () => {
try {
const holder = holderElm.value
const sudtArgs = sudtElm.value
const receiver = receiverElm.value
const { tx, total } = await generateTxToRecycleSUDTCells(holder, sudtArgs, receiver)
setOverview(
t('Generate_TX_to_recycle_sudt_cells', {
total: BI.from(total)
.div(10 ** 8)
.toString(),
cellCount: tx.inputs.length,
}).toString(),
)

const nodeType = holder.startsWith('ckb') ? 'mainnet' : 'testnet'

const formatedTx = await exportTxToSign({ json: tx, nodeType })

const blob = new Blob([JSON.stringify(formatedTx, undefined, 2)])
const filename = `tx_to_recycle_cells.json`
downloadFile(blob, filename)
} catch (e) {
if (e instanceof Error) {
setErr(e.message)
}
} finally {
setIsLoading(false)
}
}
void handle()
}

return (
<form className={styles.container} onSubmit={handleSubmit}>
<div id="recycle_cells">{t('Recycle_SUDT_Cells')}</div>
<div>{t('Recycle_SUDT_Cells_Tip')}</div>
<fieldset>
<label htmlFor="holder">{t('Holder_Address')}</label>
<input id="holder" placeholder={t('Holder_Address')!} />
</fieldset>
<fieldset>
<label htmlFor="sudt">{t('Args_of_SUDT')}</label>
<input id="sudt" placeholder={t('Args_of_SUDT')!} />
</fieldset>
<fieldset>
<label htmlFor="receiver">{t('Receiver_Address')}</label>
<input id="receiver" placeholder={t('Receiver_Address')!} />
</fieldset>
{err ? <div className={styles.err}>{err}</div> : <div className={styles.overview}>{overview}</div>}
<Button style={{ width: 144 }} disabled={isLoading} type="submit">
{isLoading ? <div className={styles.loading} /> : t('Generate')}
</Button>
</form>
)
}

export default RecycleCells
2 changes: 1 addition & 1 deletion packages/neuron/src/pages/tools/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
gap: 16px;
align-items: flex-start;
justify-content: center;
margin-bottom: 144px;
margin-bottom: 24px;
padding: 24px;
background: linear-gradient(180deg, rgb(54 54 54 / 40%) 0%, rgb(29 29 29 / 20%) 100%);
border: 1px solid rgb(255 255 255 / 20%);
Expand Down
4 changes: 4 additions & 0 deletions packages/neuron/src/pages/tools/index.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Page } from '../../components/Page'
import styles from './index.module.scss'
import { Button } from '../../components/Button'
import exportTxToSign, { JSONFormatError } from '../../utils/export-tx-to-sign'
import RecycleCells from './RecycleCells'

interface PageProps {}

Expand Down Expand Up @@ -175,6 +176,9 @@ const Download: NextPage<PageProps> = () => {
{isProcess ? <RefreshSvg /> : null}
</Button>
</div>
<div className={styles.body}>
<RecycleCells />
</div>
</Page>
)
}
Expand Down
11 changes: 11 additions & 0 deletions packages/neuron/src/utils/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const downloadFile = (blob: Blob | MediaSource, filename: string) => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', filename)
document.body.appendChild(link)
link.click()
URL.revokeObjectURL(url)
document.body.removeChild(link)
}
2 changes: 2 additions & 0 deletions packages/neuron/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CKB_MAINNET_NODE_URL = 'https://mainnet.ckb.dev/rpc'
export const CKB_TESTNET_NODE_URL = 'https://testnet.ckb.dev/rpc'
4 changes: 1 addition & 3 deletions packages/neuron/src/utils/export-tx-to-sign.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { type Script, utils } from '@ckb-lumos/base'

const CKB_MAINNET_NODE_URL = 'https://mainnet.ckb.dev/rpc'
const CKB_TESTNET_NODE_URL = 'https://testnet.ckb.dev/rpc'
import { CKB_MAINNET_NODE_URL, CKB_TESTNET_NODE_URL } from './constants'

interface Transaction {
hash: string
Expand Down
2 changes: 2 additions & 0 deletions packages/neuron/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './algolia'
export * from './route'
export * from './env'
export * from './node'
export * from './browser'
export * from './constants'

0 comments on commit cd9ae83

Please sign in to comment.