Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multisig import and export config compatible with ckb-cli #2442

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions packages/neuron-wallet/src/controllers/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,35 @@ import fs from 'fs'
import path from 'path'
import { dialog, BrowserWindow } from 'electron'
import { t } from 'i18next'
import { addressToScript, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'
import { addressToScript, scriptToAddress, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'
import { ResponseCode } from 'utils/const'
import MultisigConfig from 'database/chain/entities/multisig-config'
import MultisigConfigModel from 'models/multisig-config'
import MultisigService from 'services/multisig'
import CellsService from 'services/cells'
import OfflineSignService from 'services/offline-sign'
import Multisig from 'models/multisig'
import SystemScriptInfo from 'models/system-script-info'
import NetworksService from 'services/networks'

interface MultisigConfigOutput {
multisig_configs: Record<string, {
sighash_addresses: string[],
require_first_n: number,
threshold: number
alias?: string
}>
}

const validateImportConfig = (configOutput: MultisigConfigOutput) => {
return configOutput.multisig_configs &&
Object.values(configOutput.multisig_configs).length &&
Object.values(configOutput.multisig_configs).every(
config => config.sighash_addresses?.length >= Math.max(+config.require_first_n, +config.threshold
)
)
}

const SPEC256_BLAKE160_LEN = 42
export default class MultisigController {
// eslint-disable-next-line prettier/prettier
#multisigService: MultisigService;
Expand All @@ -25,7 +45,7 @@ export default class MultisigController {
m: number
n: number
blake160s: string[]
alias: string
alias?: string
}) {
const multiSignConfig = MultisigConfig.fromModel(new MultisigConfigModel(
params.walletId,
Expand Down Expand Up @@ -104,27 +124,18 @@ export default class MultisigController {
}
try {
const json = fs.readFileSync(filePaths[0], 'utf-8')
let configs: Array<{r: number, m: number, n: number, blake160s: string[], alias: string}> = JSON.parse(json)
if (!Array.isArray(configs)) {
configs = [configs]
}
if (
configs.some(config => config.r === undefined
|| config.m === undefined
|| config.n === undefined
|| config.blake160s === undefined
|| config.r > config.n
|| config.m > config.n
|| !config.blake160s.length
|| config.blake160s.some(v => v.length !== SPEC256_BLAKE160_LEN)
)
) {
const configOutput: MultisigConfigOutput = JSON.parse(json)
if (!validateImportConfig(configOutput)) {
dialog.showErrorBox(t('common.error'), t('messages.invalid-json'))
return
}
const saveConfigs = configs.map(config => ({
...config,
const saveConfigs = Object.values(configOutput.multisig_configs).map(config => ({
r: +config.require_first_n,
m: +config.threshold,
n: config.sighash_addresses.length,
blake160s: config.sighash_addresses.map(v => addressToScript(v).args),
walletId,
alias: config.alias
}))
const savedResult = await Promise.allSettled(saveConfigs.map(config => this.saveConfig(config)))
const saveSuccessConfigs: MultisigConfig[] = []
Expand Down Expand Up @@ -169,8 +180,19 @@ export default class MultisigController {
if (canceled || !filePath) {
return
}
const isMainnet = NetworksService.getInstance().isMainnet()
const output: MultisigConfigOutput = { multisig_configs: {} }
configs.forEach(v => {
output.multisig_configs[Multisig.hash(v.blake160s, v.r, v.m, v.n)] = {
sighash_addresses: v.blake160s.map(args => scriptToAddress(SystemScriptInfo.generateSecpScript(args), isMainnet)),
require_first_n: v.r,
threshold: v.m,
alias: v.alias
}
})


fs.writeFileSync(filePath, JSON.stringify(configs))
fs.writeFileSync(filePath, JSON.stringify(output, undefined, 2))

dialog.showMessageBox({
type: 'info',
Expand Down
196 changes: 127 additions & 69 deletions packages/neuron-wallet/tests/controllers/multisig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ResponseCode } from '../../src/utils/const'
import MultisigService from '../../src/services/multisig'
import MultisigController from '../../src/controllers/multisig'
import CellsService from '../../src/services/cells'
import { scriptToAddress, systemScripts } from '@nervosnetwork/ckb-sdk-utils'

let response = 0
let dialogRes = { canceled: false, filePaths: ['./'], filePath: './' }
Expand All @@ -21,26 +22,10 @@ jest.mock('electron', () => ({
jest.mock('../../src/services/multisig')
const MultiSigServiceMock = MultisigService as jest.MockedClass<typeof MultisigService>

let fileContent: {
r?: number
m?: number
n?: number
blake160s?: string[]
} | {
r?: number
m?: number
n?: number
blake160s?: string[]
}[] = [{
r: 1,
m: 2,
n: 3,
blake160s: ['0xcdef55dcb787257236bbe8d8c338951b4290ca69', '0x3403fcbbd9e20fa31e722eb9981b2203ad475904']
}]

const readFileSyncMock = jest.fn()
jest.mock('fs', () => {
return {
readFileSync: () => JSON.stringify(fileContent),
readFileSync: () => readFileSyncMock(),
writeFileSync: () => jest.fn(),
existsSync: () => jest.fn()
}
Expand Down Expand Up @@ -72,31 +57,39 @@ jest.mock('../../src/services/offline-sign', () => ({
loadTransactionJSON: () => loadTransactionJSONMock()
}))

const multisigArgs = '0x40518821915b81de0614d8c45dbef77151a22ad1'
const multisigBlake160s = [
'0xcdef55dcb787257236bbe8d8c338951b4290ca69',
'0x3403fcbbd9e20fa31e722eb9981b2203ad475904',
'0xc75e25d1a08c03617fd7211607a0a7479ad2ec31'
]
const multisigConfig = {
testnet: {
params: {
r: 1,
m: 2,
n: 3,
blake160s: [
'0xcdef55dcb787257236bbe8d8c338951b4290ca69',
'0x3403fcbbd9e20fa31e722eb9981b2203ad475904',
'0xc75e25d1a08c03617fd7211607a0a7479ad2ec31'
],
multisig_configs: {
sighash_addresses: multisigBlake160s.map(args => scriptToAddress({
args,
codeHash: systemScripts.SECP256K1_BLAKE160.codeHash,
hashType: systemScripts.SECP256K1_BLAKE160.hashType
}, false)),
require_first_n: 1,
threshold: 2
},
isMainnet: false
},
result: 'ckt1qpw9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn32sq2q2xyzry2ms80qv9xcc3wmaam32x3z45gut5d40'
},
mainnet: {
params: {
r: 1,
m: 2,
n: 3,
blake160s: [
'0xcdef55dcb787257236bbe8d8c338951b4290ca69',
'0x3403fcbbd9e20fa31e722eb9981b2203ad475904',
'0xc75e25d1a08c03617fd7211607a0a7479ad2ec31'
],
multisig_configs: {
sighash_addresses: multisigBlake160s.map(args => scriptToAddress({
args,
codeHash: systemScripts.SECP256K1_BLAKE160.codeHash,
hashType: systemScripts.SECP256K1_BLAKE160.hashType
}, true)),
require_first_n: 1,
threshold: 2
},
isMainnet: true
},
result: 'ckb1qpw9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn32sq2q2xyzry2ms80qv9xcc3wmaam32x3z45gjelzlh'
Expand Down Expand Up @@ -153,77 +146,142 @@ describe('test for multisig controller', () => {
})

describe('import config', () => {
beforeEach(() => {
readFileSyncMock.mockReset()
})
it('cancel import', async () => {
dialogRes = { canceled: true, filePaths: [], filePath: './' }
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
})
it('import data is error', async () => {
fileContent = [{
...multisigConfig.testnet.params,
r: undefined
}]
it('no multisig_configs', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({}))
await multisigController.importConfig('1234')
expect(showErrorBoxMock).toHaveBeenCalledWith()
}),
it('multisig_configs is empty', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({ multisig_configs: {} }))
await multisigController.importConfig('1234')
expect(showErrorBoxMock).toHaveBeenCalledWith()
}),
it('import data is error no require_first_n', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
require_first_n: undefined
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is error no threshold', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
threshold: undefined
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is error require_first_n is not number', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
require_first_n: 'dd'
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is error threshold is not number', async () => {
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
threshold: 'undefined'
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is invalidation r > n', async () => {
fileContent = [{
...multisigConfig.testnet.params,
r: 4
}]
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
require_first_n: 4
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is invalidation m > n', async () => {
fileContent = [{
...multisigConfig.testnet.params,
m: 4
}]
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
threshold: 4
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is invalidation blake160s empty', async () => {
fileContent = [{
...multisigConfig.testnet.params,
blake160s: []
}]
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
sighash_addresses: []
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import data is invalidation blake160 length not 42', async () => {
fileContent = [{
...multisigConfig.testnet.params,
blake160s: ['0xcdef55dcb787257236bbe8d8c338951b4290ca6911']
}]
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: {
...multisigConfig.testnet.params.multisig_configs,
sighash_addresses: [multisigConfig.testnet.params.multisig_configs.sighash_addresses[0]]
}
}
}))
const res = await multisigController.importConfig('1234')
expect(res).toBeUndefined()
expect(showErrorBoxMock).toHaveBeenCalledWith()
})
it('import object success', async () => {
fileContent = multisigConfig.testnet.params
readFileSyncMock.mockReturnValue(JSON.stringify({
multisig_configs: {
[multisigArgs]: multisigConfig.testnet.params.multisig_configs
}
}))
MultiSigServiceMock.prototype.saveMultisigConfig.mockResolvedValueOnce({
...multisigConfig.testnet.params,
blake160s: multisigBlake160s,
id: 1,
walletId: '1234',
alias: ''
} as any)
const res = await multisigController.importConfig('1234')
expect(res?.result[0].blake160s).toBe(multisigConfig.testnet.params.blake160s)
})
it('import success', async () => {
fileContent = multisigConfig.testnet.params
MultiSigServiceMock.prototype.saveMultisigConfig.mockResolvedValueOnce({
...multisigConfig.testnet.params,
id: 1,
walletId: '1234',
alias: '',
} as any)
const res = await multisigController.importConfig('1234')
expect(res?.result[0].blake160s).toBe(multisigConfig.testnet.params.blake160s)
expect(res?.result[0].blake160s).toBe(multisigBlake160s)
})
})

Expand Down