Skip to content

Commit 7ed6e70

Browse files
committed
Merge branch 'tbaut-values-callinfo-generic' of github.com:ChainSafe/Multix into tbaut-values-callinfo-generic
2 parents 563bbda + da3c510 commit 7ed6e70

File tree

8 files changed

+210
-213
lines changed

8 files changed

+210
-213
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"prettier": "^3.0.3"
3535
},
3636
"resolutions": {
37-
"graphql": "^16.0.0"
37+
"graphql": "^16.0.0",
38+
"@polkadot/util-crypto": "12.5.1",
39+
"@polkadot/util": "12.5.1"
3840
}
3941
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { injectedAccounts } from './injectedAccounts'
2+
3+
export const knownMultisigs = {
4+
'test-multisig-1': {
5+
address: '5CmwqwwLEkEtsmB9gFaTJdCfurz33xyggHnvwHaGKtvmQNxq',
6+
threshold: 2,
7+
signatories: [injectedAccounts[0].address, injectedAccounts[1].address]
8+
}
9+
}

packages/ui/cypress/support/commands.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference types="cypress" />
22

33
import { AuthRequests, Extension, TxRequests } from './Extension'
4+
import { MultisigInfo, rejectCurrentMultisigTxs } from '../utils/rejectCurrentMultisigTxs'
45
import { InjectedAccountWitMnemonic } from '../fixtures/injectedAccounts'
56

67
// ***********************************************
@@ -77,6 +78,8 @@ Cypress.Commands.add('rejectTx', (id: number, reason: string) => {
7778
return extension.rejectTx(id, reason)
7879
})
7980

81+
Cypress.Commands.add('rejectCurrentMultisigTx', rejectCurrentMultisigTxs)
82+
8083
declare global {
8184
namespace Cypress {
8285
interface Chainable {
@@ -86,43 +89,61 @@ declare global {
8689
* @example cy.initExtension([{ address: '7NPoMQbiA6trJKkjB35uk96MeJD4PGWkLQLH7k7hXEkZpiba', name: 'Alice', type: 'sr25519'}])
8790
*/
8891
initExtension: (accounts: InjectedAccountWitMnemonic[]) => Chainable<AUTWindow>
92+
8993
/**
9094
* Read the authentication request queue.
9195
* @example cy.getAuthRequests().then((authQueue) => { cy.wrap(Object.values(authQueue).length).should("eq", 1) })
9296
*/
9397
getAuthRequests: () => Chainable<AuthRequests>
98+
9499
/**
95100
* Authorize a specific request
96101
* @param {number} id - the id of the request to authorize. This id is part of the getAuthRequests object response.
97102
* @param {string[]} accountAddresses - the account addresses to share with the applications. These addresses must be part of the ones shared in the `initExtension`
98103
* @example cy.enableAuth(1694443839903, ["7NPoMQbiA6trJKkjB35uk96MeJD4PGWkLQLH7k7hXEkZpiba"])
99104
*/
100105
enableAuth: (id: number, accountAddresses: string[]) => void
106+
101107
/**
102108
* Reject a specific request
103109
* @param {number} id - the id of the request to reject. This id is part of the getAuthRequests object response.
104110
* @param {reason} reason - the reason for the rejection
105111
* @example cy.rejectAuth(1694443839903, "Cancelled")
106112
*/
107113
rejectAuth: (id: number, reason: string) => void
114+
108115
/**
109116
* Read the tx request queue.
110117
* @example cy.getTxRequests().then((txQueue) => { cy.wrap(Object.values(txQueue).length).should("eq", 1) })
111118
*/
112119
getTxRequests: () => Chainable<TxRequests>
120+
113121
/**
114122
* Authorize a specific request
115123
* @param {number} id - the id of the request to approve. This id is part of the getTxRequests object response.
116124
* @example cy.approveTx(1694443839903)
117125
*/
118126
approveTx: (id: number) => void
127+
119128
/**
120129
* Reject a specific request
121130
* @param {number} id - the id of the tx request to reject. This id is part of the getTxRequests object response.
122131
* @param {reason} reason - the reason for the rejection
123132
* @example cy.rejectAuth(1694443839903, "Cancelled")
124133
*/
125134
rejectTx: (id: number, reason: string) => void
135+
136+
/**
137+
* Reject all pending multisig requests with a specific account
138+
* @param {InjectedAccountWitMnemonic} opt.account - The account to reject pending transactions with. It should be the proposer
139+
* @param {multisigInfo} opt.multisigInfo - The information about the multisig to remove pending transactions from
140+
* @param {WSendpoint} opt.WSendpoint - The RPC endpoint to connect to to submit the rejection batch transaction
141+
*/
142+
rejectCurrentMultisigTx: (opt: {
143+
account: InjectedAccountWitMnemonic
144+
multisigInfo: MultisigInfo
145+
WSendpoint: string
146+
}) => void
126147
}
127148
}
128149
}

packages/ui/cypress/tests/transactions.cy.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { injectedAccounts } from '../fixtures/injectedAccounts'
2+
import { knownMultisigs } from '../fixtures/knownMultisigs'
23
import { landingPageUrl } from '../fixtures/landingData'
34
import { landingPage } from '../support/page-objects/landingPage'
45
import { multisigPage } from '../support/page-objects/multisigPage'
@@ -51,7 +52,18 @@ describe('Perform transactions', () => {
5152
})
5253
})
5354

54-
it.skip('Makes a balance transfer with Alice', () => {
55+
it('Makes a balance transfer with Alice', () => {
56+
cy.rejectCurrentMultisigTx({
57+
account: injectedAccounts[0],
58+
multisigInfo: {
59+
address: knownMultisigs['test-multisig-1'].address,
60+
threshold: knownMultisigs['test-multisig-1'].threshold,
61+
otherSignatories: knownMultisigs['test-multisig-1'].signatories.filter(
62+
(address) => address !== injectedAccounts[0].address
63+
)
64+
},
65+
WSendpoint: 'wss://rococo-rpc.polkadot.io'
66+
})
5567
multisigPage.newTransactionButton().click()
5668
sendTxModal.sendTxTitle().should('be.visible')
5769
fillAndSubmitTransactionForm()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { cryptoWaitReady } from '@polkadot/util-crypto'
2+
import { InjectedAccountWitMnemonic } from '../fixtures/injectedAccounts'
3+
import { Keyring, WsProvider, ApiPromise } from '@polkadot/api'
4+
import { MultisigStorageInfo } from '../../src/types'
5+
import { PendingTx } from '../../src/hooks/usePendingTx'
6+
import { SubmittableExtrinsic } from '@polkadot/api/types'
7+
import { ISubmittableResult, AnyTuple, Codec } from '@polkadot/types/types'
8+
import { StorageKey } from '@polkadot/types'
9+
10+
export interface MultisigInfo {
11+
address: string
12+
otherSignatories: string[]
13+
threshold: number
14+
}
15+
const callBack =
16+
(resolve: (thenableOrResult?: unknown) => void) =>
17+
({ status, txHash }: ISubmittableResult) => {
18+
console.log('Transaction status:', status.type)
19+
if (status.isBroadcast) {
20+
console.log('Broadcasted', txHash.toHex())
21+
}
22+
23+
if (status.isInBlock) {
24+
console.log('In block')
25+
}
26+
27+
if (status.isFinalized) {
28+
console.log('Finalized block hash', status.asFinalized.toHex())
29+
resolve()
30+
}
31+
}
32+
33+
const getPendingMultisixTx = (
34+
multisigTxs: [StorageKey<AnyTuple>, Codec][],
35+
multisigInfo: MultisigInfo
36+
) => {
37+
const curratedMultisigTxs: PendingTx[] = []
38+
39+
multisigTxs.forEach((storage) => {
40+
// this is supposed to be the multisig address that we asked the storage for
41+
const multisigFromChain = (storage[0].toHuman() as Array<string>)[0]
42+
const hash = (storage[0].toHuman() as Array<string>)[1]
43+
const info = storage[1].toJSON() as unknown as MultisigStorageInfo
44+
45+
// Fix for ghost proposals for https://github.com/polkadot-js/apps/issues/9103
46+
// These 2 should be the same
47+
if (multisigFromChain.toLowerCase() !== multisigInfo.address.toLowerCase()) {
48+
console.error(
49+
'The onchain call and the one found in the block donot correspond',
50+
multisigFromChain,
51+
multisigInfo.address
52+
)
53+
return
54+
}
55+
56+
curratedMultisigTxs.push({
57+
hash,
58+
info,
59+
from: multisigInfo.address
60+
})
61+
})
62+
63+
return curratedMultisigTxs
64+
}
65+
66+
const getRejectionsTxs = (
67+
pendingMultisigTxs: PendingTx[],
68+
account: InjectedAccountWitMnemonic,
69+
multisigInfo: MultisigInfo,
70+
api: ApiPromise
71+
) => {
72+
const allTxs: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []
73+
pendingMultisigTxs.forEach((pendingMultisigTx) => {
74+
const depositor = pendingMultisigTx.info.depositor
75+
if (depositor !== account.address) {
76+
console.log('multisig tx not proposed by the same account', depositor, account.address)
77+
return
78+
}
79+
80+
const rejectCurrent = api.tx.multisig.cancelAsMulti(
81+
multisigInfo.threshold,
82+
multisigInfo.otherSignatories,
83+
pendingMultisigTx.info.when,
84+
pendingMultisigTx.hash
85+
)
86+
allTxs.push(rejectCurrent)
87+
})
88+
89+
return allTxs
90+
}
91+
92+
export const rejectCurrentMultisigTxs = ({
93+
account,
94+
multisigInfo,
95+
WSendpoint
96+
}: {
97+
account: InjectedAccountWitMnemonic
98+
multisigInfo: MultisigInfo
99+
WSendpoint: string
100+
}) => {
101+
// this function takes some time waiting for a finalized block
102+
// with the removal of all pending tx. We set a max timout of 30s
103+
return cy.then(
104+
{ timeout: 30000 },
105+
() =>
106+
new Cypress.Promise(async (resolve) => {
107+
await cryptoWaitReady()
108+
109+
const keyring = new Keyring({ type: 'sr25519' })
110+
keyring.addFromMnemonic(account.mnemonic)
111+
112+
const wsProvider = new WsProvider(WSendpoint)
113+
const api = await ApiPromise.create({ provider: wsProvider })
114+
115+
const multisigTxs = await api.query.multisig.multisigs.entries(multisigInfo.address)
116+
const pendingMultisigTxs = getPendingMultisixTx(multisigTxs, multisigInfo)
117+
118+
if (!pendingMultisigTxs.length) {
119+
console.log('no pending multisig tx for', multisigInfo.address)
120+
resolve()
121+
return
122+
}
123+
124+
console.log('pendingMultisigTxs', pendingMultisigTxs)
125+
126+
const allTxs = getRejectionsTxs(pendingMultisigTxs, account, multisigInfo, api)
127+
128+
console.log(`The multisig has ${allTxs.length} pending txs. Rejecting them now`)
129+
130+
api.tx.utility
131+
.batchAll(allTxs)
132+
.signAndSend(keyring.getPair(account.address), callBack(resolve))
133+
})
134+
)
135+
}

packages/ui/src/hooks/useGetEncodedAddress.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const useGetEncodedAddress = () => {
88

99
const getEncodedAddress = useCallback(
1010
(address: string | Uint8Array | undefined) => {
11-
if (!chainInfo || !address) {
11+
if (!chainInfo || !address || address === 'undefined') {
1212
return
1313
}
1414

packages/ui/src/hooks/usePendingTx.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const usePendingTx = (multiProxy?: MultiProxy) => {
4949
// These 2 should be the same
5050
if (multisigFromChain.toLowerCase() !== multisigs[index].toLowerCase()) {
5151
console.error(
52-
'The onchain call and the one found in the block donot correstpond',
52+
'The onchain call and the one found in the block donot correspond',
5353
multisigFromChain,
5454
multisigs[index]
5555
)

0 commit comments

Comments
 (0)