diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c955bb3a4..b7fb104e01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 0.33.2 (2020-11-26) + +[CKB v0.35.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.35.1) was released on Sept. 14th, 2020. This version of CKB node is now bundled and preconfigured in Neuron. + +### Hotfix + +* Upgrade Asset Account script config. + +### Bug fix + +* Prevent Neuron from quitting when the main window is closed on MacOS. + + # 0.33.1 (2020-11-16) [CKB v0.35.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.35.1) was released on Sept. 14th, 2020. This version of CKB node is now bundled and preconfigured in Neuron. diff --git a/lerna.json b/lerna.json index 2aefbf1d77..052b2b660f 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.33.1", + "version": "0.33.2", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index e6c7f81990..359890fe80 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.33.1", + "version": "0.33.2", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index fbffd062f1..99621cee5b 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.33.1", + "version": "0.33.2", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/src/components/PasswordRequest/index.tsx b/packages/neuron-ui/src/components/PasswordRequest/index.tsx index 25be441e7e..7d7d744df6 100644 --- a/packages/neuron-ui/src/components/PasswordRequest/index.tsx +++ b/packages/neuron-ui/src/components/PasswordRequest/index.tsx @@ -13,6 +13,7 @@ import { sendTransaction, deleteWallet, backupWallet, + migrateAcp, sendCreateSUDTAccountTransaction, sendSUDTTransaction, } from 'states' @@ -93,6 +94,16 @@ const PasswordRequest = () => { }) break } + case 'migrate-acp': { + await migrateAcp({ id: walletID, password })(dispatch).then(status => { + if (isSuccessResponse({ status })) { + history.push(RoutePath.History) + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } case 'unlock': { if (isSending) { break @@ -183,7 +194,7 @@ const PasswordRequest = () => {

{t(`password-request.${actionType}.title`)}

- {['unlock', 'create-sudt-account', 'send-sudt'].includes(actionType ?? '') ? null : ( + {['unlock', 'create-sudt-account', 'send-sudt', 'migrate-acp'].includes(actionType ?? '') ? null : (
{wallet ? wallet.name : null}
)} { const existingAccountNames = accounts.filter(acc => acc.accountName).map(acc => acc.accountName || '') + useEffect(() => { + checkMigrateAcp().then(res => { + if (isSuccessResponse(res)) { + if (res.result === false) { + history.push(RoutePath.Overview) + } + } else { + dispatch({ + type: AppActions.AddNotification, + payload: { + type: 'alert', + timestamp: +new Date(), + content: typeof res.message === 'string' ? res.message : res.message.content, + }, + }) + } + }) + }, [dispatch, history]) + useEffect(() => { const ckbBalance = BigInt(balance) const isInsufficient = (res: { status: number }) => diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts index f64542db9c..58b848e78c 100644 --- a/packages/neuron-ui/src/containers/Main/hooks.ts +++ b/packages/neuron-ui/src/containers/Main/hooks.ts @@ -214,6 +214,16 @@ export const useSubscription = ({ }) break } + case 'migrate-acp': { + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: payload || '', + actionType: 'migrate-acp', + }, + }) + break + } default: { break } diff --git a/packages/neuron-ui/src/exceptions/address.ts b/packages/neuron-ui/src/exceptions/address.ts index 72e03d3e4f..661595d19e 100644 --- a/packages/neuron-ui/src/exceptions/address.ts +++ b/packages/neuron-ui/src/exceptions/address.ts @@ -22,3 +22,10 @@ export class AddressEmptyException extends Error { super(`${I18N_PATH}${ErrorCode.AddressIsEmpty}`) } } + +export class AddressDeprecatedException extends Error { + public code = ErrorCode.AddressIsDeprecated + constructor() { + super(`${I18N_PATH}${ErrorCode.AddressIsDeprecated}`) + } +} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index ebb10d4ef8..b03328cdee 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -295,6 +295,9 @@ }, "send-sudt": { "title": "Send sUDT" + }, + "migrate-acp": { + "title": "Upgrade Asset Accounts" } }, "qrcode": { @@ -407,7 +410,8 @@ "305": "$t(messages.fields.address) cannot be empty.", "306": "Please enter a mainnet address", "307": "Please enter a testnet address", - "308": "Amount is not enough" + "308": "Amount is not enough", + "309": "The receiver needs to upgrade her account address to accept more transfer." } }, "sync": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 18cfa472b8..621fc86d63 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -295,6 +295,9 @@ }, "send-sudt": { "title": "發起 sUDT 交易" + }, + "migrate-acp": { + "title": "升级资产账户" } }, "qrcode": { @@ -407,7 +410,8 @@ "305": "$t(messages.fields.address)不能為空。", "306": "請輸入主網地址", "307": "請輸入測試網地址", - "308": "餘額不足" + "308": "餘額不足", + "309": "收款人需要升級資產賬戶才能繼續接收轉賬。" } }, "sync": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 56b23e5e9c..064602a452 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -295,6 +295,9 @@ }, "send-sudt": { "title": "发起 sUDT 交易" + }, + "migrate-acp": { + "title": "升级资产账户" } }, "qrcode": { @@ -407,7 +410,8 @@ "305": "$t(messages.fields.address)不能为空。", "306": "请输入主网地址", "307": "请输入测试网地址", - "308": "余额不足" + "308": "余额不足", + "309": "收款人需要升级资产账户才能继续接收转账。" } }, "sync": { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index 719896bfd8..a3ea1f5634 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -100,6 +100,8 @@ type Action = | 'generate-send-all-to-anyone-can-pay-tx' | 'send-to-anyone-can-pay' | 'get-token-info-list' + | 'migrate-acp' + | 'check-migrate-acp' export const remoteApi =

(action: Action) => async (params: P): Promise> => { const res: SuccessFromController | FailureFromController = await ipcRenderer.invoke(action, params).catch(() => ({ @@ -126,13 +128,13 @@ export const remoteApi =

(action: Action) => async (params: P) if (isSuccessResponse(res)) { return { status: ResponseCode.SUCCESS, - result: res.result || null, + result: res.result ?? null, } } return { - status: res.status || ResponseCode.FAILURE, - message: typeof res.message === 'string' ? { content: res.message } : res.message || '', + status: res.status ?? ResponseCode.FAILURE, + message: typeof res.message === 'string' ? { content: res.message } : res.message ?? '', } } diff --git a/packages/neuron-ui/src/services/remote/sudt.ts b/packages/neuron-ui/src/services/remote/sudt.ts index b854db9538..1557e35707 100644 --- a/packages/neuron-ui/src/services/remote/sudt.ts +++ b/packages/neuron-ui/src/services/remote/sudt.ts @@ -27,3 +27,9 @@ export const generateSendAllSUDTTransaction = remoteApi('send-to-anyone-can-pay') + +export const checkMigrateAcp = remoteApi( + 'check-migrate-acp' +) + +export const migrateAcp = remoteApi('migrate-acp') diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/sudt.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/sudt.ts index 7afe8f1aba..6163478388 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/sudt.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/sudt.ts @@ -1,7 +1,8 @@ -import { ErrorCode, isSuccessResponse, ResponseCode } from 'utils' +import { ErrorCode, isSuccessResponse, ResponseCode, failureResToNotification } from 'utils' import { sendCreateSUDTAccountTransaction as sendCreateAccountTx, sendSUDTTransaction as sendSUDTTx, + migrateAcp as migrateAcpIpc, } from 'services/remote' import { AppActions, StateDispatch } from '../reducer' import { addNotification } from './app' @@ -71,3 +72,30 @@ export const sendSUDTTransaction = (params: Controller.SendSUDTTransaction.Param }) } } + +export const migrateAcp = (params: Controller.MigrateAcp.Params) => async (dispatch: StateDispatch) => { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: true }, + }) + try { + const res = await migrateAcpIpc(params) + if (res.status !== ErrorCode.PasswordIncorrect) { + dispatch({ + type: AppActions.DismissPasswordRequest, + }) + if (!isSuccessResponse(res)) { + addNotification(failureResToNotification(res))(dispatch) + } + } + return res.status + } catch (err) { + console.warn(err) + return 0 + } finally { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + } +} diff --git a/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json b/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json index b33f331006..c3a100065b 100644 --- a/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json +++ b/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json @@ -21,6 +21,28 @@ ], "expected": "ckb1qyq5lv479ewscx3ms620sv34pgeuz6zagaaqklhtgg" }, + "anyone can pay address on lina": { + "params": [ + { + "codeHash": "0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354", + "hashType": "type", + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a" + }, + true + ], + "expected": "ckb1qypylv479ewscx3ms620sv34pgeuz6zagaaqvrugu7" + }, + "anyone can pay address on aggron": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a" + }, + false + ], + "expected": "ckt1qypylv479ewscx3ms620sv34pgeuz6zagaaq3xzhsz" + }, "full version address of hashType = 'data'": { "params": [ { @@ -42,5 +64,16 @@ false ], "expected": "ckt1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlydkr98kkxrtvuag8z2j8w4pkw2k6k4l5c02auef" + }, + "full version address when args length is not matched": { + "params": [ + { + "args": "0x7346c078cd8684ba9fc7bcaba49442a13b46617c4504009400f00020", + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type" + }, + false + ], + "expected": "ckt1q3w9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn32su6xcpuvmp5yh20u009t5j2y9gfmgeshc3gyqz2qpuqqyqjlmnq6" } } diff --git a/packages/neuron-ui/src/tests/validators/sudtAddress/fixtures.ts b/packages/neuron-ui/src/tests/validators/sudtAddress/fixtures.ts index 6dc3b4759e..1f3e2386de 100644 --- a/packages/neuron-ui/src/tests/validators/sudtAddress/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/sudtAddress/fixtures.ts @@ -49,7 +49,7 @@ export default { }, exception: ErrorCode.FieldInvalid, }, - "Should throw an error when it's not a 0x04(type id ver.) address on testnet": { + "Should throw an error when it's code hash index is not 0x04": { params: { address: 'ckt1q2r2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yy3d90uh', isMainnet: false, @@ -57,17 +57,9 @@ export default { }, exception: ErrorCode.FieldInvalid, }, - "Should throw an error when it's not a 0x02(data ver.) address on mainnet": { - params: { - address: 'ckb1qjda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xw3vumhs9nvu786dj9p0q5elx66t24n3kxgj53qks', - isMainnet: true, - required: false, - }, - exception: ErrorCode.FieldInvalid, - }, 'Should throw an error when minimum is malformed': { params: { - address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yyyg3zy4fq3q6', + address: 'ckt1qy6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4v7tvzu37rv87kyv59ltdece09usz9t9yyyg3zy428nc2', isMainnet: false, required: false, }, @@ -75,15 +67,15 @@ export default { }, 'Should throw an error when the address is required but missing': { params: { - address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yywhe92q', + address: '', isMainnet: false, - required: false, + required: true, }, - exception: null, + exception: ErrorCode.FieldRequired, }, 'Should pass when the address is an acp address without code hash validation': { params: { - address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yywhe92q', + address: 'ckt1qs6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4v7tvzu37rv87kyv59ltdece09usz9t9yym9pmex', isMainnet: false, required: false, }, @@ -91,7 +83,7 @@ export default { }, 'Should throw an error when the address is an acp address but code hash is not matched': { params: { - address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yywhe92q', + address: 'ckt1qs6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4v7tvzu37rv87kyv59ltdece09usz9t9yym9pmex', codeHash: '0x123', isMainnet: false, required: false, @@ -100,11 +92,27 @@ export default { }, 'Should pass when the address is an acp address and the code hash is matched': { params: { - address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yywhe92q', - codeHash: '0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b', + address: 'ckt1qs6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4v7tvzu37rv87kyv59ltdece09usz9t9yym9pmex', + codeHash: '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', isMainnet: false, required: false, }, exception: null, }, + 'Should throw an error when address is a deprecated acp address on lina': { + params: { + address: 'ckt1qg8mxsu48mncexvxkzgaa7mz2g25uza4zpz062relhjmyuc52ps3z7tvzu37rv87kyv59ltdece09usz9t9yyx9r0yj', + isMainnet: false, + required: false, + }, + exception: ErrorCode.AddressIsDeprecated, + }, + 'Should throw an error when address is a deprecated acp address on aggron': { + params: { + address: 'ckt1qjr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yywhe92q', + isMainnet: false, + required: false, + }, + exception: ErrorCode.AddressIsDeprecated, + }, } diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index baa81c6f70..f389b58d21 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -81,7 +81,15 @@ declare namespace State { } interface PasswordRequest { - readonly actionType: 'send' | 'backup' | 'delete' | 'unlock' | 'create-sudt-account' | 'send-sudt' | null + readonly actionType: + | 'send' + | 'backup' + | 'delete' + | 'unlock' + | 'create-sudt-account' + | 'send-sudt' + | 'migrate-acp' + | null readonly walletID: string } diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index abcab6f4ec..785cdb9cfd 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -276,6 +276,18 @@ declare namespace Controller { type Response = Hash } + namespace CheckMigrateAcp { + type Params = void + type Response = boolean | undefined + } + + namespace MigrateAcp { + interface Params { + id: string + password: string + } + } + namespace ExportTransactions { interface Params { walletID: string diff --git a/packages/neuron-ui/src/types/Subject/index.d.ts b/packages/neuron-ui/src/types/Subject/index.d.ts index 6d5e2a8c04..80614bba31 100644 --- a/packages/neuron-ui/src/types/Subject/index.d.ts +++ b/packages/neuron-ui/src/types/Subject/index.d.ts @@ -7,7 +7,7 @@ interface NeuronWalletSubject { } declare namespace Command { - type Type = 'navigate-to-url' | 'delete-wallet' | 'backup-wallet' + type Type = 'navigate-to-url' | 'delete-wallet' | 'backup-wallet' | 'migrate-acp' type Payload = string | null } diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts index 98cbdf9488..191333b010 100644 --- a/packages/neuron-ui/src/utils/enums.ts +++ b/packages/neuron-ui/src/utils/enums.ts @@ -93,6 +93,7 @@ export enum ErrorCode { MainnetAddressRequired = 306, TestnetAddressRequired = 307, BalanceNotEnough = 308, + AddressIsDeprecated = 309, } export enum SyncStatus { @@ -122,16 +123,31 @@ export enum DefaultLockInfo { CodeHash = '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', HashType = 'type', CodeHashIndex = '0x00', + ArgsLen = '20', } export enum MultiSigLockInfo { CodeHash = '0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8', HashType = 'type', CodeHashIndex = '0x01', + ArgsLen = '20', } -export enum AnyoneCanPayLockInfo { - CodeHash = '0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b', +export enum AnyoneCanPayLockInfoOnAggron { + CodeHash = '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', HashType = 'type', CodeHashIndex = '0x02', + ArgsLen = '20,21,22', +} + +export enum AnyoneCanPayLockInfoOnLina { + CodeHash = '0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354', + HashType = 'type', + CodeHashIndex = '0x02', + ArgsLen = '20,21,22', +} + +export enum DeprecatedScript { + AcpOnLina = '0x020fb343953ee78c9986b091defb6252154e0bb51044fd2879fde5b27314506111', + AcpOnAggron = '0x0486a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b', } diff --git a/packages/neuron-ui/src/utils/scriptToAddress.ts b/packages/neuron-ui/src/utils/scriptToAddress.ts index 3bc7a6b777..776b200f1f 100644 --- a/packages/neuron-ui/src/utils/scriptToAddress.ts +++ b/packages/neuron-ui/src/utils/scriptToAddress.ts @@ -1,11 +1,14 @@ import { ckbCore } from 'services/chain' -import { MultiSigLockInfo, DefaultLockInfo } from './enums' +import { MultiSigLockInfo, DefaultLockInfo, AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina } from './enums' export const scriptToAddress = (lock: CKBComponents.Script, isMainnet: boolean) => { const addressPrefix = isMainnet ? ckbCore.utils.AddressPrefix.Mainnet : ckbCore.utils.AddressPrefix.Testnet - const foundLock = [MultiSigLockInfo, DefaultLockInfo].find( - info => lock.codeHash === info.CodeHash && lock.hashType === info.HashType + const foundLock = [MultiSigLockInfo, DefaultLockInfo, AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina].find( + info => + lock.codeHash === info.CodeHash && + lock.hashType === info.HashType && + info.ArgsLen.split(',').includes(`${(lock.args.length - 2) / 2}`) ) if (foundLock) { diff --git a/packages/neuron-ui/src/utils/validators/sudtAddress.ts b/packages/neuron-ui/src/utils/validators/sudtAddress.ts index 3002842751..7bd280d949 100644 --- a/packages/neuron-ui/src/utils/validators/sudtAddress.ts +++ b/packages/neuron-ui/src/utils/validators/sudtAddress.ts @@ -1,6 +1,7 @@ import { ckbCore } from 'services/chain' -import { FieldRequiredException, FieldInvalidException } from 'exceptions' -import { LONG_TYPE_PREFIX, LONG_DATA_PREFIX } from 'utils/const' +import { FieldRequiredException, FieldInvalidException, AddressDeprecatedException } from 'exceptions' +import { LONG_TYPE_PREFIX } from 'utils/const' +import { DeprecatedScript } from 'utils/enums' import { validateAddress } from './address' export const validateSUDTAddress = ({ @@ -19,13 +20,11 @@ export const validateSUDTAddress = ({ validateAddress(address, isMainnet) const parsed = ckbCore.utils.parseAddress(address, 'hex') - /** - * Only anyone can pay address(type id ver/data ver) is under validation - * - data version for mainnet - * - type version for testnet - */ - const addrType = isMainnet ? LONG_DATA_PREFIX : LONG_TYPE_PREFIX - if (!parsed.startsWith(addrType)) { + if ([DeprecatedScript.AcpOnAggron, DeprecatedScript.AcpOnLina].some(script => parsed.startsWith(script))) { + throw new AddressDeprecatedException() + } + + if (!parsed.startsWith(LONG_TYPE_PREFIX)) { throw new FieldInvalidException(FIELD_NAME) } diff --git a/packages/neuron-wallet/.env b/packages/neuron-wallet/.env index 5022422acd..f4a5e9ef2e 100644 --- a/packages/neuron-wallet/.env +++ b/packages/neuron-wallet/.env @@ -1,22 +1,37 @@ +# mainnet MAINNET_SUDT_DEP_TXHASH=0xc7813f6a415144643970c2e88e0bb6ca6a8edc5dd7c1022746f628284a9936d5 MAINNET_SUDT_DEP_INDEX=0 MAINNET_SUDT_DEP_TYPE=code MAINNET_SUDT_SCRIPT_CODEHASH=0x5e7a36a77e68eecc013dfa2fe6a23f3b6c344b04005808694ae6dd45eea4cfd5 MAINNET_SUDT_SCRIPT_HASHTYPE=type -MAINNET_ACP_DEP_TXHASH=0xa05f28c9b867f8c5682039c10d8e864cf661685252aa74a008d255c33813bb81 + +MAINNET_ACP_DEP_TXHASH=0x4153a2014952d7cac45f285ce9a7c5c0c0e1b21f2d378b82ac1433cb11c25c4d MAINNET_ACP_DEP_INDEX=0 MAINNET_ACP_DEP_TYPE=depGroup -MAINNET_ACP_SCRIPT_CODEHASH=0x0fb343953ee78c9986b091defb6252154e0bb51044fd2879fde5b27314506111 -MAINNET_ACP_SCRIPT_HASHTYPE=data +MAINNET_ACP_SCRIPT_CODEHASH=0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354 +MAINNET_ACP_SCRIPT_HASHTYPE=type + +LEGACY_MAINNET_ACP_DEP_TXHASH=0xa05f28c9b867f8c5682039c10d8e864cf661685252aa74a008d255c33813bb81 +LEGACY_MAINNET_ACP_DEP_INDEX=0 +LEGACY_MAINNET_ACP_DEP_TYPE=depGroup +LEGACY_MAINNET_ACP_SCRIPT_CODEHASH=0x0fb343953ee78c9986b091defb6252154e0bb51044fd2879fde5b27314506111 +LEGACY_MAINNET_ACP_SCRIPT_HASHTYPE=data -## testnet +# testnet TESTNET_SUDT_DEP_TXHASH=0xc1b2ae129fad7465aaa9acc9785f842ba3e6e8b8051d899defa89f5508a77958 TESTNET_SUDT_DEP_INDEX=0 TESTNET_SUDT_DEP_TYPE=code TESTNET_SUDT_SCRIPT_CODEHASH=0x48dbf59b4c7ee1547238021b4869bceedf4eea6b43772e5d66ef8865b6ae7212 TESTNET_SUDT_SCRIPT_HASHTYPE=data -TESTNET_ACP_DEP_TXHASH=0x4f32b3e39bd1b6350d326fdfafdfe05e5221865c3098ae323096f0bfc69e0a8c + +TESTNET_ACP_DEP_TXHASH=0xec26b0f85ed839ece5f11c4c4e837ec359f5adc4420410f6453b1f6b60fb96a6 TESTNET_ACP_DEP_INDEX=0 TESTNET_ACP_DEP_TYPE=depGroup -TESTNET_ACP_SCRIPT_CODEHASH=0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b +TESTNET_ACP_SCRIPT_CODEHASH=0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356 TESTNET_ACP_SCRIPT_HASHTYPE=type + +LEGACY_TESTNET_ACP_DEP_TXHASH=0x4f32b3e39bd1b6350d326fdfafdfe05e5221865c3098ae323096f0bfc69e0a8c +LEGACY_TESTNET_ACP_DEP_INDEX=0 +LEGACY_TESTNET_ACP_DEP_TYPE=depGroup +LEGACY_TESTNET_ACP_SCRIPT_CODEHASH=0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b +LEGACY_TESTNET_ACP_SCRIPT_HASHTYPE=type diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index 04bcd6ae1f..11c44bb8e0 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -3,7 +3,7 @@ "productName": "Neuron", "description": "CKB Neuron Wallet", "homepage": "https://www.nervos.org/", - "version": "0.33.1", + "version": "0.33.2", "private": true, "author": { "name": "Nervos Core Dev", @@ -79,7 +79,7 @@ "electron-notarize": "0.2.1", "jest-when": "2.7.2", "lint-staged": "9.2.5", - "neuron-ui": "0.33.1", + "neuron-ui": "0.33.2", "rimraf": "3.0.0", "ttypescript": "1.5.10" } diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts index 44725f0b1e..709eaed62b 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts @@ -88,7 +88,8 @@ export default class IndexerCacheService { for (const addressMeta of this.addressMetas) { const lockScripts = [ addressMeta.generateDefaultLockScript(), - addressMeta.generateACPLockScript() + addressMeta.generateACPLockScript(), + addressMeta.generateLegacyACPLockScript() ] for (const lockScript of lockScripts) { diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts index aed267e2ce..8f653d2d98 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts @@ -152,46 +152,44 @@ export default class Queue { } for (const [, tx] of transactions.entries()) { - const [shouldSave, , anyoneCanPayInfos] = await new TxAddressFinder( + const [, , anyoneCanPayInfos] = await new TxAddressFinder( this.lockHashes, this.anyoneCanPayLockHashes, tx, this.multiSignBlake160s ).addresses() - if (shouldSave) { - for (const [inputIndex, input] of tx.inputs.entries()) { - const previousTxHash = input.previousOutput!.txHash - const previousTxWithStatus: TransactionWithStatus | undefined = cachedPreviousTxs.get(previousTxHash) - if (!previousTxWithStatus) { - continue - } + for (const [inputIndex, input] of tx.inputs.entries()) { + const previousTxHash = input.previousOutput!.txHash + const previousTxWithStatus: TransactionWithStatus | undefined = cachedPreviousTxs.get(previousTxHash) + if (!previousTxWithStatus) { + continue + } - const previousTx = previousTxWithStatus!.transaction - const previousOutput = previousTx.outputs![+input.previousOutput!.index] - const previousOutputData = previousTx.outputsData![+input.previousOutput!.index] - input.setLock(previousOutput.lock) - previousOutput.type && input.setType(previousOutput.type) - input.setData(previousOutputData) - input.setCapacity(previousOutput.capacity) - input.setInputIndex(inputIndex.toString()) - - if ( - previousOutput.type?.computeHash() === SystemScriptInfo.DAO_SCRIPT_HASH && - previousTx.outputsData![+input.previousOutput!.index] === '0x0000000000000000' - ) { - const output = tx.outputs![inputIndex] - if (output) { - output.setDepositOutPoint(new OutPoint( - input.previousOutput!.txHash, - input.previousOutput!.index, - )) - } + const previousTx = previousTxWithStatus!.transaction + const previousOutput = previousTx.outputs![+input.previousOutput!.index] + const previousOutputData = previousTx.outputsData![+input.previousOutput!.index] + input.setLock(previousOutput.lock) + previousOutput.type && input.setType(previousOutput.type) + input.setData(previousOutputData) + input.setCapacity(previousOutput.capacity) + input.setInputIndex(inputIndex.toString()) + + if ( + previousOutput.type?.computeHash() === SystemScriptInfo.DAO_SCRIPT_HASH && + previousTx.outputsData![+input.previousOutput!.index] === '0x0000000000000000' + ) { + const output = tx.outputs![inputIndex] + if (output) { + output.setDepositOutPoint(new OutPoint( + input.previousOutput!.txHash, + input.previousOutput!.index, + )) } } - await TransactionPersistor.saveFetchTx(tx) - for (const info of anyoneCanPayInfos) { - await AssetAccountService.checkAndSaveAssetAccountWhenSync(info.tokenID, info.blake160) - } + } + await TransactionPersistor.saveFetchTx(tx) + for (const info of anyoneCanPayInfos) { + await AssetAccountService.checkAndSaveAssetAccountWhenSync(info.tokenID, info.blake160) } await this.checkAndGenerateAddressesByTx(tx) diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 9bb256d9b4..4a88d25d56 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -23,7 +23,7 @@ import CustomizedAssetsController from './customized-assets' import SystemScriptInfo from 'models/system-script-info' import logger from 'utils/logger' import AssetAccountController from './asset-account' -import { GenerateCreateAssetAccountTxParams, SendCreateAssetAccountTxParams, UpdateAssetAccountParams } from './asset-account' +import { GenerateCreateAssetAccountTxParams, SendCreateAssetAccountTxParams, UpdateAssetAccountParams, MigrateACPParams } from './asset-account' import AnyoneCanPayController from './anyone-can-pay' import { GenerateAnyoneCanPayTxParams, GenerateAnyoneCanPayAllTxParams, SendAnyoneCanPayTxParams } from './anyone-can-pay' @@ -59,6 +59,10 @@ export default class ApiController { // params: walletID this.walletsController.requestPassword(params, command) } + + if (command === 'migrate-acp') { + this.assetAccountController.showACPMigrationDialog(false) + } } private registerHandlers() { @@ -415,6 +419,15 @@ export default class ApiController { return this.assetAccountController.getAccount(params) }) + handle('check-migrate-acp', async () => { + const allowMultipleOpen = true + return this.assetAccountController.showACPMigrationDialog(allowMultipleOpen) + }) + + handle('migrate-acp', async (_, params: MigrateACPParams) => { + return this.assetAccountController.migrateAcp(params) + }) + handle('generate-send-to-anyone-can-pay-tx', async (_, params: GenerateAnyoneCanPayTxParams) => { return this.anyoneCanPayController.generateTx(params) }) diff --git a/packages/neuron-wallet/src/controllers/app/index.ts b/packages/neuron-wallet/src/controllers/app/index.ts index 41ed69bbf7..8190b15333 100644 --- a/packages/neuron-wallet/src/controllers/app/index.ts +++ b/packages/neuron-wallet/src/controllers/app/index.ts @@ -120,9 +120,6 @@ export default class AppController { }) this.mainWindow.on('closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } this.clearOnClosed() }) diff --git a/packages/neuron-wallet/src/controllers/app/subscribe.ts b/packages/neuron-wallet/src/controllers/app/subscribe.ts index e12b50ffdd..6949f68ded 100644 --- a/packages/neuron-wallet/src/controllers/app/subscribe.ts +++ b/packages/neuron-wallet/src/controllers/app/subscribe.ts @@ -33,6 +33,8 @@ export const subscribe = (dispatcher: AppResponder) => { SyncedBlockNumberSubject.getSubject().pipe(sampleTime(1000)).subscribe(params => { dispatcher.sendMessage('synced-block-number-updated', params) + + dispatcher.runCommand('migrate-acp', params) }) CommandSubject.subscribe(params => { diff --git a/packages/neuron-wallet/src/controllers/asset-account.ts b/packages/neuron-wallet/src/controllers/asset-account.ts index cb598b763c..c291676141 100644 --- a/packages/neuron-wallet/src/controllers/asset-account.ts +++ b/packages/neuron-wallet/src/controllers/asset-account.ts @@ -7,6 +7,13 @@ import NetworksService from "services/networks" import AddressGenerator from "models/address-generator" import { AddressPrefix } from "@nervosnetwork/ckb-sdk-utils" import AssetAccountInfo from "models/asset-account-info" +import TransactionSender from "services/transaction-sender" +import { BrowserWindow, dialog } from "electron" +import { t } from "i18next" +import WalletsService from "services/wallets" +import CommandSubject from 'models/subjects/command' +import SyncApiController, { SyncStatus } from "./sync-api" +import { TransactionGenerator } from "services/tx" export interface GenerateCreateAssetAccountTxParams { walletID: string @@ -34,7 +41,14 @@ export interface UpdateAssetAccountParams { decimal?: string } +export interface MigrateACPParams { + id: string + password: string +} + export default class AssetAccountController { + private displayedACPMigrationDialogByWalletIds: Set = new Set() + public async getAll(params: { walletID: string }): Promise> { const assetAccountInfo = new AssetAccountInfo() @@ -140,4 +154,92 @@ export default class AssetAccountController { result, } } + + public async migrateAcp(params: MigrateACPParams): Promise> { + const tx = await TransactionGenerator.generateMigrateLegacyACPTx(params.id) + + const txHash = await new TransactionSender().sendTx(params.id, tx!, params.password) + + const I18N_PATH = `messageBox.acp-migration-completed` + + dialog.showMessageBox({ + type: 'info', + buttons: ['ok'].map(label => t(`${I18N_PATH}.buttons.${label}`)), + defaultId: 1, + title: t(`${I18N_PATH}.title`), + message: t(`${I18N_PATH}.message`), + cancelId: 0, + noLink: true, + }) + + return { + status: ResponseCode.Success, + result: txHash, + } + } + + public async showACPMigrationDialog(allowMultipleOpen: boolean | undefined): Promise> { + const walletsService = WalletsService.getInstance() + const currentWallet = walletsService.getCurrent() + const walletId = currentWallet!.id; + + if (!allowMultipleOpen && this.displayedACPMigrationDialogByWalletIds.has(walletId)) { + return { + status: ResponseCode.Success + } + } + + const syncStatus = await new SyncApiController().getSyncStatus() + if (syncStatus !== SyncStatus.SyncCompleted || BrowserWindow.getAllWindows().length !== 1) { + return { + status: ResponseCode.Success + } + } + + const window = BrowserWindow.getFocusedWindow() + if (!window) { + return { + status: ResponseCode.Success + } + } + + const tx = await TransactionGenerator.generateMigrateLegacyACPTx(walletId) + if (!tx) { + return { + status: ResponseCode.Success + } + } + + this.displayedACPMigrationDialogByWalletIds.add(walletId) + + const I18N_PATH = `messageBox.acp-migration` + return dialog.showMessageBox({ + type: 'info', + buttons: ['skip', 'migrate'].map(label => t(`${I18N_PATH}.buttons.${label}`)), + defaultId: 1, + title: t(`${I18N_PATH}.title`), + message: t(`${I18N_PATH}.message`), + detail: t(`${I18N_PATH}.detail`), + cancelId: 0, + noLink: true, + }).then(({ response }) => { + switch (response) { + case 1: { + CommandSubject.next({ + winID: window.id, + type: 'migrate-acp', + payload: walletId, + dispatchToUI: true + }) + return true + } + case 0: + default: + return false + } + }).then(result => ({ + status: ResponseCode.Success, + result, + })) + } } diff --git a/packages/neuron-wallet/src/controllers/sync-api.ts b/packages/neuron-wallet/src/controllers/sync-api.ts index 5b15f8e14e..b64bf024d9 100644 --- a/packages/neuron-wallet/src/controllers/sync-api.ts +++ b/packages/neuron-wallet/src/controllers/sync-api.ts @@ -1,12 +1,29 @@ import SyncedBlockNumber from 'models/synced-block-number' import EventEmiter from 'events' +import NetworksService from 'services/networks' +import RpcService from 'services/rpc-service' +import { ConnectionStatus, getLatestConnectionStatus } from 'models/subjects/node' +import SyncController from './sync' + +const BUFFER_BLOCK_NUMBER = 10 +const MAX_TIP_BLOCK_DELAY = 180000 + +export enum SyncStatus { + SyncNotStart, + SyncPending, + Syncing, + SyncCompleted, +} -// Handle channel messages with EventEmiter in the main process -// @TODO: EventEmiter should replace with MessagePort in the future Worker Thread refactor export default class SyncApiController { #syncedBlockNumber = new SyncedBlockNumber() static emiter = new EventEmiter() + private TEN_MINS = 10 * 60 * 1000 + private blockNumber10MinAgo: string = '' + private timestamp10MinAgo: number | undefined + private prevUrl: string | undefined + public async mount() { this.registerHandlers() } @@ -16,4 +33,55 @@ export default class SyncApiController { this.#syncedBlockNumber.setNextBlock(BigInt(blockNumber)) }) } + + public async getSyncStatus () { + const [ + syncedBlockNumber = '0', + connectionStatus, + ] = await Promise.all([ + new SyncController().currentBlockNumber() + .then(res => { + if (res.status) { + return res.result.currentBlockNumber + } + return '0' + }) + .catch(() => '0'), + getLatestConnectionStatus(), + ]) + + const network = NetworksService.getInstance().getCurrent() + const tipHeader = await new RpcService(network.remote).getTipHeader() + const tipBlockNumber = tipHeader.number + const tipBlockTimestamp = Number(tipHeader.timestamp) + const currentTimestamp = Date.now() + const url = (connectionStatus as ConnectionStatus).url + + if ((!this.timestamp10MinAgo && tipBlockNumber !== '') || (this.prevUrl && url !== this.prevUrl && tipBlockNumber !== '')) { + this.timestamp10MinAgo = currentTimestamp + this.blockNumber10MinAgo = tipBlockNumber + this.prevUrl = url + } + + const now = Math.floor(currentTimestamp / 1000) * 1000 + if (BigInt(syncedBlockNumber) < BigInt(0) || tipBlockNumber === '0' || tipBlockNumber === '') { + return SyncStatus.SyncNotStart + } + + if (this.timestamp10MinAgo && this.timestamp10MinAgo + this.TEN_MINS < currentTimestamp) { + if (BigInt(this.blockNumber10MinAgo) >= BigInt(tipBlockNumber)) { + return SyncStatus.SyncPending + } + this.timestamp10MinAgo = currentTimestamp + this.blockNumber10MinAgo = tipBlockNumber + } + + if (BigInt(syncedBlockNumber) + BigInt(BUFFER_BLOCK_NUMBER) < BigInt(tipBlockNumber)) { + return SyncStatus.Syncing + } + if (tipBlockTimestamp + MAX_TIP_BLOCK_DELAY >= now) { + return SyncStatus.SyncCompleted + } + return SyncStatus.Syncing + } } diff --git a/packages/neuron-wallet/src/database/address/meta.ts b/packages/neuron-wallet/src/database/address/meta.ts index eefd9638bb..a0e4d1e08e 100644 --- a/packages/neuron-wallet/src/database/address/meta.ts +++ b/packages/neuron-wallet/src/database/address/meta.ts @@ -107,4 +107,9 @@ export default class AddressMeta implements Address { const assetAccountInfo = new AssetAccountInfo() return assetAccountInfo.generateAnyoneCanPayScript(this.blake160) } + + public generateLegacyACPLockScript(): Script { + const assetAccountInfo = new AssetAccountInfo() + return assetAccountInfo.generateLegacyAnyoneCanPayScript(this.blake160) + } } diff --git a/packages/neuron-wallet/src/locales/en.ts b/packages/neuron-wallet/src/locales/en.ts index 05da2d2572..9b1debf92b 100644 --- a/packages/neuron-wallet/src/locales/en.ts +++ b/packages/neuron-wallet/src/locales/en.ts @@ -140,6 +140,22 @@ export default { 'install-and-exit': 'Install and Exit' } }, + 'acp-migration': { + title: 'Upgrade Asset Account', + message: 'Upgrade Asset Account', + detail: 'Recently our security team identified a potential vulnerability in the experimental Asset Account script. We have deployed a new Asset Account script with a fix on mainnet and all future Asset Account will use the new version. We suggest you to upgrade them to use the new script.', + buttons: { + migrate: 'Secure upgrade now', + skip: 'I know the risk, will upgrade later' + } + }, + 'acp-migration-completed': { + title: 'Congratulations! You have completed the secure upgrade.', + message: 'Congratulations! You have completed the secure upgrade.', + buttons: { + ok: 'OK' + } + } }, prompt: { password: { diff --git a/packages/neuron-wallet/src/locales/zh-tw.ts b/packages/neuron-wallet/src/locales/zh-tw.ts index b2c01b4032..16760103f9 100644 --- a/packages/neuron-wallet/src/locales/zh-tw.ts +++ b/packages/neuron-wallet/src/locales/zh-tw.ts @@ -138,6 +138,22 @@ export default { 'install-and-exit': '安裝並退出' } }, + 'acp-migration': { + title: '升級資產賬戶', + message: '升級資產賬戶', + detail: '我們的安全團隊在近期在實驗性的資產賬戶腳本中定位了壹個潛在的安全性問題。我們已經部署了新的資產賬戶腳本來替換舊腳本,未來的資產賬戶也會采納新的腳本。建議您立即升級以使用新的賬戶腳本。', + buttons: { + migrate: '安全升級', + skip: '已知風險,稍後升級' + } + }, + 'acp-migration-completed': { + title: '恭喜!您已經完成安全升級。', + message: '恭喜!您已經完成安全升級。', + buttons: { + ok: 'OK' + } + } }, prompt: { password: { diff --git a/packages/neuron-wallet/src/locales/zh.ts b/packages/neuron-wallet/src/locales/zh.ts index 066d19193b..c0987d776b 100644 --- a/packages/neuron-wallet/src/locales/zh.ts +++ b/packages/neuron-wallet/src/locales/zh.ts @@ -139,6 +139,22 @@ export default { 'install-and-exit': '安装并退出' } }, + 'acp-migration': { + title: '升级资产账户', + message: '升级资产账户', + detail: '我们的安全团队在近期在实验性的资产账户脚本中定位了一个潜在的安全性问题。我们已经部署了新的资产账户脚本来替换旧脚本,未来的资产账户也会采纳新的脚本。建议您立即升级以使用新的账户脚本。', + buttons: { + migrate: '安全升级', + skip: '已知风险,稍后升级' + } + }, + 'acp-migration-completed': { + title: '恭喜!您已经完成安全升级。', + message: '恭喜!您已经完成安全升级。', + buttons: { + ok: 'OK' + } + } }, prompt: { password: { diff --git a/packages/neuron-wallet/src/main.ts b/packages/neuron-wallet/src/main.ts index db36c176d6..1469d3b34e 100644 --- a/packages/neuron-wallet/src/main.ts +++ b/packages/neuron-wallet/src/main.ts @@ -23,6 +23,12 @@ if (singleInstanceLock) { app.on('second-instance', () => { appController.restoreWindow() }) + + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } + }) } else { app.quit() } diff --git a/packages/neuron-wallet/src/models/asset-account-info.ts b/packages/neuron-wallet/src/models/asset-account-info.ts index d43e432f6f..1edde7a8af 100644 --- a/packages/neuron-wallet/src/models/asset-account-info.ts +++ b/packages/neuron-wallet/src/models/asset-account-info.ts @@ -12,6 +12,7 @@ export interface ScriptCellInfo { export default class AssetAccountInfo { private sudtInfo: ScriptCellInfo private anyoneCanPayInfo: ScriptCellInfo + private legacyAnyoneCanPayInfo: ScriptCellInfo private static MAINNET_GENESIS_BLOCK_HASH: string = '0x92b197aa1fba0f63633922c61c92375c9c074a93e85963554f5499fe1450d0e5' @@ -37,6 +38,12 @@ export default class AssetAccountInfo { codeHash: process.env.MAINNET_ACP_SCRIPT_CODEHASH!, hashType: process.env.MAINNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType } + this.legacyAnyoneCanPayInfo = { + cellDep: new CellDep(new OutPoint(process.env.LEGACY_MAINNET_ACP_DEP_TXHASH!, process.env.LEGACY_MAINNET_ACP_DEP_INDEX!), + process.env.LEGACY_MAINNET_ACP_DEP_TYPE! as DepType), + codeHash: process.env.LEGACY_MAINNET_ACP_SCRIPT_CODEHASH!, + hashType: process.env.LEGACY_MAINNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType + } } else { this.sudtInfo = { cellDep: new CellDep(new OutPoint(process.env.TESTNET_SUDT_DEP_TXHASH!, process.env.TESTNET_SUDT_DEP_INDEX!), @@ -50,6 +57,12 @@ export default class AssetAccountInfo { codeHash: process.env.TESTNET_ACP_SCRIPT_CODEHASH!, hashType: process.env.TESTNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType } + this.legacyAnyoneCanPayInfo = { + cellDep: new CellDep(new OutPoint(process.env.LEGACY_TESTNET_ACP_DEP_TXHASH!, process.env.LEGACY_TESTNET_ACP_DEP_INDEX!), + process.env.LEGACY_TESTNET_ACP_DEP_TYPE! as DepType), + codeHash: process.env.LEGACY_TESTNET_ACP_SCRIPT_CODEHASH!, + hashType: process.env.LEGACY_TESTNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType + } } } @@ -65,6 +78,10 @@ export default class AssetAccountInfo { return this.anyoneCanPayInfo.codeHash } + public getLegacyAnyoneCanPayInfo(): ScriptCellInfo { + return this.legacyAnyoneCanPayInfo + } + public generateSudtScript(args: string): Script { return new Script(this.sudtInfo.codeHash, args, this.sudtInfo.hashType) } @@ -74,6 +91,11 @@ export default class AssetAccountInfo { return new Script(info.codeHash, args, info.hashType) } + public generateLegacyAnyoneCanPayScript(args: string): Script { + const info = this.legacyAnyoneCanPayInfo + return new Script(info.codeHash, args, info.hashType) + } + public isSudtScript(script: Script): boolean { return script.codeHash === this.sudtInfo.codeHash && script.hashType === this.sudtInfo.hashType } diff --git a/packages/neuron-wallet/src/models/subjects/command.ts b/packages/neuron-wallet/src/models/subjects/command.ts index 096a813ead..7fae92621a 100644 --- a/packages/neuron-wallet/src/models/subjects/command.ts +++ b/packages/neuron-wallet/src/models/subjects/command.ts @@ -2,7 +2,7 @@ import { Subject } from 'rxjs' const CommandSubject = new Subject<{ winID: number - type: 'navigate-to-url' | 'delete-wallet' | 'backup-wallet' | 'export-xpubkey' | 'import-xpubkey' + type: 'navigate-to-url' | 'delete-wallet' | 'backup-wallet' | 'export-xpubkey' | 'import-xpubkey' | 'migrate-acp' payload: string | null dispatchToUI: boolean }>() diff --git a/packages/neuron-wallet/src/models/subjects/node.ts b/packages/neuron-wallet/src/models/subjects/node.ts index ce54823196..1f07b6375d 100644 --- a/packages/neuron-wallet/src/models/subjects/node.ts +++ b/packages/neuron-wallet/src/models/subjects/node.ts @@ -1,17 +1,24 @@ import { BehaviorSubject } from 'rxjs' +import { take } from 'rxjs/operators' -export const ConnectionStatusSubject = new BehaviorSubject<{ +export type ConnectionStatus = { url: string, connected: boolean, isBundledNode: boolean, startedBundledNode: boolean, -}>({ +} + +export const ConnectionStatusSubject = new BehaviorSubject({ url: '', connected: false, isBundledNode: true, startedBundledNode: false, }) +export const getLatestConnectionStatus = async () => { + return ConnectionStatusSubject.pipe(take(1)).toPromise() +} + export default class SyncedBlockNumberSubject { private static subject = new BehaviorSubject('0') diff --git a/packages/neuron-wallet/src/services/cells.ts b/packages/neuron-wallet/src/services/cells.ts index bcc418bcbb..7572a52cd1 100644 --- a/packages/neuron-wallet/src/services/cells.ts +++ b/packages/neuron-wallet/src/services/cells.ts @@ -18,6 +18,7 @@ import Output from 'models/chain/output' import SystemScriptInfo from 'models/system-script-info' import Script, { ScriptHashType } from 'models/chain/script' import LiveCellService from './live-cell-service' +import AssetAccountInfo from 'models/asset-account-info' export const MIN_CELL_CAPACITY = '6100000000' @@ -844,4 +845,28 @@ export default class CellsService { return uniqueBlake160s } + + public static async gatherLegacyACPInputs (walletId: string) { + const assetAccountInfo = new AssetAccountInfo() + const legacyACPScriptInfo = assetAccountInfo.getLegacyAnyoneCanPayInfo() + const outputs = await getConnection() + .getRepository(OutputEntity) + .createQueryBuilder('output') + .where({ + status: OutputStatus.Live, + lockCodeHash: legacyACPScriptInfo.codeHash, + lockHashType: legacyACPScriptInfo.hashType, + }) + .andWhere(` + lockArgs IN ( + SELECT publicKeyInBlake160 + FROM hd_public_key_info + WHERE walletId = :walletId + )`, + { walletId } + ) + .getMany() + + return outputs + } } diff --git a/packages/neuron-wallet/src/services/tx/transaction-generator.ts b/packages/neuron-wallet/src/services/tx/transaction-generator.ts index a93ff3d014..54ebf44a62 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-generator.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-generator.ts @@ -1,4 +1,4 @@ -import CellsService from 'services/cells' +import CellsService, { MIN_CELL_CAPACITY } from 'services/cells' import { CapacityTooSmall } from 'exceptions' import FeeMode from 'models/fee-mode' import TransactionSize from 'models/transaction-size' @@ -766,6 +766,83 @@ export class TransactionGenerator { return tx } + public static async generateMigrateLegacyACPTx (walletId: string): Promise { + const assetAccountInfo = new AssetAccountInfo() + const legacyACPCells = await CellsService.gatherLegacyACPInputs(walletId) + if (!legacyACPCells.length) { + return null + } + const legacyACPInputs = legacyACPCells.map(cell => { + return new Input( + cell.outPoint(), + '0', + cell.capacity, + cell.lockScript(), + cell.lockHash + ); + }); + const ACPCells = legacyACPCells.map(cell => { + const newACPLockScript = assetAccountInfo.generateAnyoneCanPayScript(cell.lockArgs) + + cell.lockCodeHash = newACPLockScript.codeHash + cell.lockHashType = newACPLockScript.hashType + cell.lockArgs = newACPLockScript.args + cell.lockHash = newACPLockScript.computeHash() + + return cell + }) + const ACPOutputs = ACPCells.map(cell => cell.toModel()) + + const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() + const sudtCellDep = assetAccountInfo.sudtCellDep + const anyoneCanPayDep = assetAccountInfo.anyoneCanPayCellDep + const legacyACPCellDep = assetAccountInfo.getLegacyAnyoneCanPayInfo().cellDep + + const tx = Transaction.fromObject({ + version: '0', + headerDeps: [], + cellDeps: [secpCellDep, sudtCellDep, anyoneCanPayDep, legacyACPCellDep], + inputs: legacyACPInputs, + outputs: ACPOutputs, + outputsData: ACPCells.map(o => o.data || '0x'), + witnesses: [], + }) + + const baseSize = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() * tx.inputs.length + + const inputGatherResult = await CellsService.gatherInputs( + '0', + walletId, + '0', + '1000', + baseSize, + TransactionGenerator.CHANGE_OUTPUT_DATA_SIZE, + TransactionGenerator.CHANGE_OUTPUT_SIZE, + ) + tx.inputs.push(...inputGatherResult.inputs) + + const originalChangeCapacity = inputGatherResult.inputs + .reduce((sum: bigint, input: Input) => { + return sum + BigInt(input.capacity) + }, BigInt(0)) + + const actualChangeCapacity = originalChangeCapacity - BigInt(inputGatherResult.finalFee) + tx.fee = inputGatherResult.finalFee + + if (actualChangeCapacity < BigInt(MIN_CELL_CAPACITY)) { + throw new CapacityNotEnough() + } + + const changeOutput = new Output( + actualChangeCapacity.toString(), + SystemScriptInfo.generateSecpScript(inputGatherResult.inputs[0].lock!.args!) + ) + tx.outputs.push(changeOutput) + tx.outputsData.push('0x') + + return tx; + } + private static checkTxCapacity(tx: Transaction, msg: string) { const inputCapacity = tx.inputs.map(i => BigInt(i.capacity!)).reduce((result, c) => result + c, BigInt(0)) const outputCapacity = tx.outputs.map(o => BigInt(o.capacity!)).reduce((result, c) => result + c, BigInt(0)) diff --git a/packages/neuron-wallet/tests/block-sync-render/indexer-cache-service.intg.test.ts b/packages/neuron-wallet/tests/block-sync-render/indexer-cache-service.intg.test.ts index 8d5badd875..b05106bcd3 100644 --- a/packages/neuron-wallet/tests/block-sync-render/indexer-cache-service.intg.test.ts +++ b/packages/neuron-wallet/tests/block-sync-render/indexer-cache-service.intg.test.ts @@ -59,6 +59,7 @@ const addressMetas = [addressMeta] const defaultLockScript = addressMeta.generateDefaultLockScript() const singleMultiSignLockScript = addressMeta.generateSingleMultiSignLockScript() const acpLockScript = addressMeta.generateACPLockScript() +const legacyAcpLockScript = addressMeta.generateLegacyACPLockScript() const formattedDefaultLockScript = { code_hash: defaultLockScript.codeHash, hash_type: defaultLockScript.hashType, @@ -74,13 +75,19 @@ const formattedAcpLockScript = { hash_type: acpLockScript.hashType, args: acpLockScript.args } +const formattedLegacyAcpLockScript = { + code_hash: legacyAcpLockScript.codeHash, + hash_type: legacyAcpLockScript.hashType, + args: legacyAcpLockScript.args +} const mockGetTransactionHashes = (mocks: any[] = []) => { const stubbedConstructor = when(stubbedTransactionCollectorConstructor) for (const lock of [ formattedDefaultLockScript, - formattedAcpLockScript + formattedAcpLockScript, + formattedLegacyAcpLockScript, ]) { const {hashes} = mocks.find(mock => mock.lock === lock) || {hashes: []} stubbedConstructor diff --git a/packages/neuron-wallet/tests/controllers/asset-account.test.ts b/packages/neuron-wallet/tests/controllers/asset-account.test.ts new file mode 100644 index 0000000000..486dcedf02 --- /dev/null +++ b/packages/neuron-wallet/tests/controllers/asset-account.test.ts @@ -0,0 +1,137 @@ + +import {SyncStatus} from '../../src/controllers/sync-api' + +const stubbedSyncApiControllerConstructor = jest.fn() +const stubbedGetSyncStatus = jest.fn() +const stubbedGetAllWindows = jest.fn() +const stubbedGetFocusedWindow = jest.fn() +const stubbedShowMessageBox = jest.fn() +const stubbedGenerateMigrateLegacyACPTx = jest.fn() +const stubbedCommandSubjectNext = jest.fn() + +const resetMocks = () => { + stubbedGetSyncStatus.mockReset() + stubbedGetAllWindows.mockReset() + stubbedShowMessageBox.mockReset() + stubbedGetFocusedWindow.mockReset() + stubbedGenerateMigrateLegacyACPTx.mockReset() + stubbedCommandSubjectNext.mockReset() +} + +describe('AssetAccountController', () => { + let assetAccountController: any; + let AssetAccountController: any; + const walletId = 'w1' + + jest.doMock('electron', () => { + return { + BrowserWindow : { + getAllWindows: stubbedGetAllWindows, + getFocusedWindow: stubbedGetFocusedWindow + }, + dialog: { + showMessageBox: stubbedShowMessageBox + } + } + }); + + jest.doMock('../../src/services/wallets', () => { + return { + getInstance : () => ({ + getCurrent: () => ({id: walletId}) + }) + } + }); + + jest.doMock('../../src/controllers/sync-api', () => ({ + __esModule: true, + default: stubbedSyncApiControllerConstructor.mockImplementation( + () => ({ + getSyncStatus: stubbedGetSyncStatus, + }) + ), + SyncStatus: SyncStatus + })); + + jest.doMock('../../src/services/tx', () => ({ + TransactionGenerator: { + generateMigrateLegacyACPTx: stubbedGenerateMigrateLegacyACPTx + } + })); + + jest.doMock('../../src/models/subjects/command', () => ({ + next: stubbedCommandSubjectNext + })); + + beforeEach(() => { + resetMocks() + AssetAccountController = require('../../src/controllers/asset-account').default + assetAccountController = new AssetAccountController() + }); + describe('#showACPMigrationDialog', () => { + const mockStates = (syncStatus: any, windowsCount: any, hasFocusedWindow: any, hasTx: any) => { + stubbedGetSyncStatus.mockResolvedValue(syncStatus) + stubbedGetAllWindows.mockReturnValue(Array(windowsCount)) + stubbedGetFocusedWindow.mockReturnValue(hasFocusedWindow ? {id: '1'} : undefined) + stubbedGenerateMigrateLegacyACPTx.mockResolvedValue(hasTx ? {} : null) + } + beforeEach(() => { + stubbedShowMessageBox.mockResolvedValue({response: 1}) + }); + describe('when all conditions met to display dialog', () => { + beforeEach(async () => { + mockStates(SyncStatus.SyncCompleted, 1, true, true) + await assetAccountController.showACPMigrationDialog() + }); + it('broadcast migrate-acp command', () => { + expect(stubbedCommandSubjectNext).toHaveBeenCalledWith({ + dispatchToUI: true, payload: "w1", type: "migrate-acp", winID: "1" + }) + }) + describe('attempts to open dialog again', () => { + beforeEach(async () => { + stubbedCommandSubjectNext.mockReset() + mockStates(SyncStatus.SyncCompleted, 1, true, true) + await assetAccountController.showACPMigrationDialog() + }); + it('should not broadcast migrate-acp command', () => { + expect(stubbedCommandSubjectNext).not.toHaveBeenCalled() + }) + }); + describe('force to open dialog again', () => { + beforeEach(async () => { + stubbedCommandSubjectNext.mockReset() + mockStates(SyncStatus.SyncCompleted, 1, true, true) + await assetAccountController.showACPMigrationDialog(true) + }); + it('broadcast migrate-acp command', () => { + expect(stubbedCommandSubjectNext).toHaveBeenCalledWith({ + dispatchToUI: true, payload: "w1", type: "migrate-acp", winID: "1" + }) + }) + }); + }); + describe('when one of the conditions not met', () => { + [ + [SyncStatus.SyncNotStart, 1, true, true], + [SyncStatus.SyncPending, 1, true, true], + [SyncStatus.Syncing, 1, true, true], + [SyncStatus.SyncCompleted, 0, true, true], + [SyncStatus.SyncCompleted, 2, true, true], + [SyncStatus.SyncCompleted, 1, false, true], + [SyncStatus.SyncCompleted, 1, true, false], + ].forEach(([syncStatus, winCount, hasFocusedWindow, hasTx]) => { + describe(`when SyncStatus: ${syncStatus}, winCount: ${winCount}, hasFocusedWindow: ${hasFocusedWindow}, hasTx: ${hasTx}`, () => { + beforeEach(async () => { + assetAccountController = new AssetAccountController() + mockStates(syncStatus, winCount, hasFocusedWindow, hasTx) + await assetAccountController.showACPMigrationDialog() + }); + it('should not display again in the application session', () => { + expect(stubbedCommandSubjectNext).not.toHaveBeenCalled() + }); + }); + }) + }); + }); +}); diff --git a/packages/neuron-wallet/tests/controllers/sync-api.test.ts b/packages/neuron-wallet/tests/controllers/sync-api.test.ts new file mode 100644 index 0000000000..3f01e0974d --- /dev/null +++ b/packages/neuron-wallet/tests/controllers/sync-api.test.ts @@ -0,0 +1,97 @@ + +const stubbedSyncControllerConstructor = jest.fn() +const stubbedRpcServiceConstructor = jest.fn() +const stubbedCurrentBlockNumber = jest.fn() +const stubbedGetLatestConnectionStatus = jest.fn() +const stubbedGetTipHeader = jest.fn() +const stubbedDateNow = jest.fn() + +const resetMocks = () => { + stubbedCurrentBlockNumber.mockReset() + stubbedGetLatestConnectionStatus.mockReset() + stubbedGetTipHeader.mockReset() + stubbedDateNow.mockReset() +} + +describe('AssetAccountController', () => { + let syncApiController: any; + + jest.doMock('../../src/models/subjects/node', () => { + return { + getLatestConnectionStatus: stubbedGetLatestConnectionStatus + } + }); + jest.doMock('../../src/services/rpc-service', () => { + return { + __esModule: true, + default: stubbedRpcServiceConstructor.mockImplementation( + () => ({ + getTipHeader: stubbedGetTipHeader, + }) + ), + } + }); + + jest.doMock('../../src/controllers/sync', () => ({ + __esModule: true, + default: stubbedSyncControllerConstructor.mockImplementation( + () => ({ + currentBlockNumber: stubbedCurrentBlockNumber, + }) + ), + })); + + + beforeEach(() => { + resetMocks() + const SyncApiController = require('../../src/controllers/sync-api').default + syncApiController = new SyncApiController() + Date.now = stubbedDateNow + }); + describe('#getSyncStatus', () => { + let syncStatus: AnalyserOptions + const mockStates = (syncedBlockNumber: any, url: any, tipHeader: any) => { + stubbedCurrentBlockNumber.mockResolvedValue({status: true, result: {currentBlockNumber: syncedBlockNumber}}) + stubbedGetLatestConnectionStatus.mockResolvedValue({url}) + stubbedGetTipHeader.mockResolvedValue(tipHeader) + } + beforeEach(() => { + stubbedDateNow.mockReturnValue('10000000') + }); + [ + ['2', 'fakeurl1', {number: '12', timestamp: '10000000'}, 3], + ['1', 'fakeurl1', {number: '12', timestamp: '10000000'}, 2], + ['1', 'fakeurl1', {number: '0', timestamp: '10000000'}, 0], + ].forEach(([syncedBlockNumber, url, tipHeader, expectedSyncStatus]) => { + const {number, timestamp} = tipHeader as any + describe(`when syncedBlockNumber: ${syncedBlockNumber}, url: ${url}, tipBlockNumber: ${number}, tipTimestamp: ${timestamp}, syncStatus: ${expectedSyncStatus}`, () => { + beforeEach(async () => { + mockStates(syncedBlockNumber, url, tipHeader) + syncStatus = await syncApiController.getSyncStatus() + }); + it(`returns expected sync status ${expectedSyncStatus}`, () => { + expect(syncStatus).toEqual(expectedSyncStatus) + }) + }); + }) + + describe('SyncPending status', () => { + describe("with the first check", () => { + beforeEach(async () => { + mockStates('2', 'fakeurl1', {number: '12', timestamp: '10000000'}) + await syncApiController.getSyncStatus() + }); + describe('with the second check 10 min later', () => { + beforeEach(async () => { + stubbedDateNow.mockReturnValue('16000000') + mockStates('2', 'fakeurl1', {number: '12', timestamp: '10000000'}) + syncStatus = await syncApiController.getSyncStatus() + }); + it(`returns expected sync pending status`, () => { + expect(syncStatus).toEqual(1) + }) + }); + }) + }); + }); +}); diff --git a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts index b5cee5c856..f8b0939538 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts @@ -99,7 +99,8 @@ describe('TransactionGenerator', () => { hasData: boolean, typeScript: Script | null, who: any = bob, - daoData?: string | undefined + daoData?: string | undefined, + outputData?: string | undefined ) => { const output = new OutputEntity() output.outPointTxHash = randomHex() @@ -119,6 +120,9 @@ describe('TransactionGenerator', () => { if (daoData) { output.daoData = daoData } + if (outputData) { + output.data = outputData + } return output } @@ -1871,4 +1875,97 @@ describe('TransactionGenerator', () => { expect(output.data).toEqual('0x' + '0'.repeat(32)) }) }) + + describe('#generateMigrateLegacyACPTx', () => { + const defaultLock = new Script(SystemScriptInfo.SECP_CODE_HASH, alice.publicKeyInBlake160, SystemScriptInfo.SECP_HASH_TYPE) + const legacyACPCodeHash: string = process.env.LEGACY_MAINNET_ACP_SCRIPT_CODEHASH as string + const legacyACPHashType: string = process.env.LEGACY_MAINNET_ACP_SCRIPT_HASHTYPE as string + const legacyACPLock = new Script(legacyACPCodeHash, alice.publicKeyInBlake160, legacyACPHashType as ScriptHashType) + const assetAccountInfo = new AssetAccountInfo() + const tokenID = '0x' + '0'.repeat(64) + + const sudtScript = assetAccountInfo.generateSudtScript(tokenID) + const acpLock = assetAccountInfo.generateAnyoneCanPayScript(alice.publicKeyInBlake160) + + describe('with legacy acp cells', () => { + beforeEach(async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Live, false, null, {lockScript: legacyACPLock}), + generateCell(toShannon('1000'), OutputStatus.Live, false, null, {lockScript: legacyACPLock}), + + generateCell(toShannon('61'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + generateCell(toShannon('100'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + generateCell(toShannon('100'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + + generateCell(toShannon('200'), OutputStatus.Live, false, sudtScript, {lockScript: legacyACPLock}, undefined, BufferUtils.writeBigUInt128LE(BigInt(100))), + generateCell(toShannon('200'), OutputStatus.Live, false, sudtScript, {lockScript: legacyACPLock}, undefined, BufferUtils.writeBigUInt128LE(BigInt(100))), + ] + await getConnection().manager.save(cells) + }); + it('generates acp migration transaction', async () => { + const tx = (await TransactionGenerator.generateMigrateLegacyACPTx(alice.walletId))! + const totalLegacyACPCellsCount = tx.inputs.filter(input => input.lockHash === legacyACPLock.computeHash()).length + const totalMigratedACPCellsCount = tx.outputs.filter(output => output.lockHash === acpLock.computeHash()).length + const totalMigratedSUDTCellCount = tx.outputs.filter(output => output.typeHash === sudtScript.computeHash()).length + const normalInputCellCapacity = tx.inputs.filter(input => input.lockHash === defaultLock.computeHash()).reduce((sum, input) => { + return sum += BigInt(input.capacity) + }, BigInt(0)) + const normalOutputCellCapacity = tx.outputs.filter(output => output.lockHash === defaultLock.computeHash()).reduce((sum, output) => { + return sum += BigInt(output.capacity) + }, BigInt(0)) + const acpCellCapacity = tx.outputs.filter(output => output.lockHash === acpLock.computeHash()).reduce((sum, output) => { + return sum += BigInt(output.capacity) + }, BigInt(0)) + const acpCellSudtAmount = tx.outputsData.reduce((sum, lehex) => { + return sum += BufferUtils.parseAmountFromSUDTData(lehex) + }, BigInt(0)) + + expect(totalLegacyACPCellsCount).toEqual(4) + expect(totalMigratedACPCellsCount).toEqual(4) + expect(normalInputCellCapacity.toString()).toEqual(toShannon('161')) + expect(normalOutputCellCapacity.toString()).toEqual((normalInputCellCapacity - BigInt(tx.fee)).toString()) + expect(acpCellCapacity.toString()).toEqual(toShannon('2400')) + expect(totalMigratedSUDTCellCount).toEqual(2) + expect(acpCellSudtAmount).toEqual(BigInt(200)) + }) + }); + describe('with no legacy acp cells', () => { + beforeEach(async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Live, false, null, {lockScript: acpLock}), + generateCell(toShannon('1000'), OutputStatus.Live, false, null, {lockScript: acpLock}), + + generateCell(toShannon('61'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + generateCell(toShannon('100'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + generateCell(toShannon('100'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + + generateCell(toShannon('200'), OutputStatus.Live, false, sudtScript, {lockScript: acpLock}, undefined, BufferUtils.writeBigUInt128LE(BigInt(100))), + generateCell(toShannon('200'), OutputStatus.Live, false, sudtScript, {lockScript: acpLock}, undefined, BufferUtils.writeBigUInt128LE(BigInt(100))), + ] + await getConnection().manager.save(cells) + }); + it('returns null', async () => { + const tx = await TransactionGenerator.generateMigrateLegacyACPTx(alice.walletId) + expect(tx).toEqual(null) + }) + }); + describe('with insufficient normal cells for fees', () => { + beforeEach(async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Live, false, null, {lockScript: legacyACPLock}), + generateCell(toShannon('61'), OutputStatus.Live, false, null, {lockScript: defaultLock}), + ] + await getConnection().manager.save(cells) + }); + it('throws CapacityNotEnough', async () => { + let error = null + try { + await TransactionGenerator.generateMigrateLegacyACPTx(alice.walletId) + } catch (err) { + error = err + } + expect(error).not.toEqual(null) + }); + }); + }); }) diff --git a/packages/neuron-wallet/tests/setup.ts b/packages/neuron-wallet/tests/setup.ts index b002594736..5cb63264da 100644 --- a/packages/neuron-wallet/tests/setup.ts +++ b/packages/neuron-wallet/tests/setup.ts @@ -47,6 +47,12 @@ jest.mock('dotenv', () => ({ process.env.MAINNET_ACP_SCRIPT_CODEHASH='0x0000000000000000000000000000000000000000000000000000000000000000' process.env.MAINNET_ACP_SCRIPT_HASHTYPE='type' + process.env.LEGACY_MAINNET_ACP_DEP_TXHASH='0x0000000000000000000000000000000000000000000000000000000000000001' + process.env.LEGACY_MAINNET_ACP_DEP_INDEX='0' + process.env.LEGACY_MAINNET_ACP_DEP_TYPE='code' + process.env.LEGACY_MAINNET_ACP_SCRIPT_CODEHASH='0x0000000000000000000000000000000000000000000000000000000000000001' + process.env.LEGACY_MAINNET_ACP_SCRIPT_HASHTYPE='type' + process.env.TESTNET_SUDT_DEP_TXHASH='0xc1b2ae129fad7465aaa9acc9785f842ba3e6e8b8051d899defa89f5508a77958' process.env.TESTNET_SUDT_DEP_INDEX='0' process.env.TESTNET_SUDT_DEP_TYPE='code' @@ -57,6 +63,12 @@ jest.mock('dotenv', () => ({ process.env.TESTNET_ACP_DEP_TYPE='depGroup' process.env.TESTNET_ACP_SCRIPT_CODEHASH='0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b' process.env.TESTNET_ACP_SCRIPT_HASHTYPE='type' + + process.env.LEGACY_TESTNET_ACP_DEP_TXHASH='0x0000000000000000000000000000000000000000000000000000000000000001' + process.env.LEGACY_TESTNET_ACP_DEP_INDEX='0' + process.env.LEGACY_TESTNET_ACP_DEP_TYPE='code' + process.env.LEGACY_TESTNET_ACP_SCRIPT_CODEHASH='0x0000000000000000000000000000000000000000000000000000000000000001' + process.env.LEGACY_TESTNET_ACP_SCRIPT_HASHTYPE='type' } }))