Skip to content

Commit

Permalink
feat: Multisig import and export config compatible with ckb-cli (#2442)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanguoyu authored Jul 18, 2022
1 parent ce2c08f commit 9b9287a
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 90 deletions.
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

1 comment on commit 9b9287a

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging for test is done in 2687797442

Please sign in to comment.