Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/siwx-single-flight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@reown/appkit-controllers': patch
'@reown/appkit-scaffold-ui': patch
---

Prevent concurrent SIWX signing actions.

Concurrent sign requests now share one in-flight operation, and the SIWX modal disables Sign and
Cancel while either action is pending.
131 changes: 74 additions & 57 deletions packages/controllers/src/utils/SIWXUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CoreHelperUtil } from './CoreHelperUtil.js'
*/

let addEmbeddedWalletSessionPromise: Promise<void> | null = null
let requestSignMessagePromise: Promise<void> | null = null

export const SIWXUtil = {
getSIWX() {
Expand Down Expand Up @@ -100,83 +101,99 @@ export const SIWXUtil = {
return sessions.length > 0
},
async requestSignMessage() {
const siwx = OptionsController.state.siwx
const address = CoreHelperUtil.getPlainAddress(ChainController.getActiveCaipAddress())
const network = getActiveCaipNetwork()

if (!siwx) {
throw new Error('SIWX is not enabled')
if (requestSignMessagePromise) {
return requestSignMessagePromise
}

if (!address) {
throw new Error('No ActiveCaipAddress found')
}
const request = (async () => {
const siwx = OptionsController.state.siwx
const address = CoreHelperUtil.getPlainAddress(ChainController.getActiveCaipAddress())
const network = getActiveCaipNetwork()

if (!network) {
throw new Error('No ActiveCaipNetwork or client found')
}
if (!siwx) {
throw new Error('SIWX is not enabled')
}

const connectorId = ConnectorController.getConnectorId(network.chainNamespace)
if (!address) {
throw new Error('No ActiveCaipAddress found')
}

try {
const siwxMessage = await siwx.createMessage({
chainId: network.caipNetworkId,
accountAddress: address
})
if (!network) {
throw new Error('No ActiveCaipNetwork or client found')
}

const message = siwxMessage.toString()
const connectorId = ConnectorController.getConnectorId(network.chainNamespace)

let signature = ''
if (siwx.signMessage) {
signature = await siwx.signMessage({
message,
try {
const siwxMessage = await siwx.createMessage({
chainId: network.caipNetworkId,
accountAddress: address,
connectorId
accountAddress: address
})
} else {
if (connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
RouterController.pushTransactionStack({})
}
signature =
(await ConnectionController.signMessage(message, {

const message = siwxMessage.toString()

let signature = ''
if (siwx.signMessage) {
signature = await siwx.signMessage({
message,
chainId: network.caipNetworkId,
accountAddress: address,
connectorId
})) || ''
}
})
} else {
if (connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
RouterController.pushTransactionStack({})
}
signature =
(await ConnectionController.signMessage(message, {
chainId: network.caipNetworkId,
accountAddress: address,
connectorId
})) || ''
}

await siwx.addSession({
data: siwxMessage,
message,
signature
})
await siwx.addSession({
data: siwxMessage,
message,
signature
})

ChainController.setLastConnectedSIWECaipNetwork(network)
ChainController.setLastConnectedSIWECaipNetwork(network)

ModalController.close()
ModalController.close()

EventsController.sendEvent({
type: 'track',
event: 'SIWX_AUTH_SUCCESS',
properties: this.getSIWXEventProperties()
})
} catch (error) {
if (!ModalController.state.open || RouterController.state.view === 'ApproveTransaction') {
await ModalController.open({
view: 'SIWXSignMessage'
EventsController.sendEvent({
type: 'track',
event: 'SIWX_AUTH_SUCCESS',
properties: this.getSIWXEventProperties()
})
} catch (error) {
if (!ModalController.state.open || RouterController.state.view === 'ApproveTransaction') {
await ModalController.open({
view: 'SIWXSignMessage'
})
}

SnackController.showError('Error signing message')
EventsController.sendEvent({
type: 'track',
event: 'SIWX_AUTH_ERROR',
properties: this.getSIWXEventProperties(error)
})

// eslint-disable-next-line no-console
console.error('SIWXUtil:requestSignMessage', error)
}
})()

SnackController.showError('Error signing message')
EventsController.sendEvent({
type: 'track',
event: 'SIWX_AUTH_ERROR',
properties: this.getSIWXEventProperties(error)
})
requestSignMessagePromise = request

// eslint-disable-next-line no-console
console.error('SIWXUtil:requestSignMessage', error)
try {
return await request
} finally {
if (requestSignMessagePromise === request) {
requestSignMessagePromise = null
}
}
},
async cancelSignMessage() {
Expand Down
7 changes: 4 additions & 3 deletions packages/controllers/tests/utils/SIWXUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('SIWXUtil', () => {
expect(getSIWXEventPropertiesSpy).toHaveBeenCalled()
})

it('should pass the immutable signing context to SIWX', async () => {
it('should use one immutable signing context for concurrent requests', async () => {
let resolveSignature: ((signature: string) => void) | undefined
const signaturePromise = new Promise<string>(resolve => {
resolveSignature = resolve
Expand All @@ -96,7 +96,8 @@ describe('SIWXUtil', () => {
)
vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue('walletConnect')

const request = SIWXUtil.requestSignMessage()
const firstRequest = SIWXUtil.requestSignMessage()
const secondRequest = SIWXUtil.requestSignMessage()

await vi.waitFor(() => expect(mockSIWX.signMessage).toHaveBeenCalledOnce())
expect(mockSIWX.signMessage).toHaveBeenCalledWith({
Expand All @@ -107,7 +108,7 @@ describe('SIWXUtil', () => {
})

resolveSignature?.('0xsignature')
await request
await Promise.all([firstRequest, secondRequest])

expect(mockSIWX.createMessage).toHaveBeenCalledOnce()
expect(mockSIWX.addSession).toHaveBeenCalledOnce()
Expand Down
10 changes: 10 additions & 0 deletions packages/scaffold-ui/src/views/w3m-siwx-sign-message-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class W3mSIWXSignMessageView extends LitElement {
fullWidth
variant="neutral-secondary"
?loading=${this.isCancelling}
?disabled=${this.isSigning || this.isCancelling}
@click=${this.onCancel.bind(this)}
data-testid="w3m-connecting-siwe-cancel"
>
Expand All @@ -59,6 +60,7 @@ export class W3mSIWXSignMessageView extends LitElement {
variant="neutral-primary"
@click=${this.onSign.bind(this)}
?loading=${this.isSigning}
?disabled=${this.isSigning || this.isCancelling}
data-testid="w3m-connecting-siwe-sign"
>
${this.isSigning ? 'Signing...' : 'Sign'}
Expand All @@ -69,6 +71,10 @@ export class W3mSIWXSignMessageView extends LitElement {

// -- Private ------------------------------------------- //
private async onSign() {
if (this.isSigning || this.isCancelling) {
return
}

this.isSigning = true
try {
await SIWXUtil.requestSignMessage()
Expand All @@ -89,6 +95,10 @@ export class W3mSIWXSignMessageView extends LitElement {
}

private async onCancel() {
if (this.isSigning || this.isCancelling) {
return
}

this.isCancelling = true
await SIWXUtil.cancelSignMessage().finally(() => (this.isCancelling = false))
}
Expand Down
Loading