Skip to content

fix: async approval add polling - #2381

Open
rukmini-basu-da wants to merge 18 commits into
mainfrom
rukmini/async-approval
Open

fix: async approval add polling#2381
rukmini-basu-da wants to merge 18 commits into
mainfrom
rukmini/async-approval

Conversation

@rukmini-basu-da

@rukmini-basu-da rukmini-basu-da commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Adds a polling mechanism and a new status awaiting-signature:
image

1.pending: prepared command not sent anywhere
2.awaiting-signature: sent to signing provider but haven't received signature yet (this happens after we hit approve the first time)
3. signed: found signature on signing provider (so as soon as we go to this activity on the ui, it starts querying the signing driver for this tx if it's in awaiting-signature and then as soon as it finds it we update the sqllite status)
4. executed: happens after we hit approve the second timeand happy path if the execute against the ledger is successful
5. failed: as is, can happen if something goes wrong at any step but would also include if something fails on the execute step

return storage.transactions.get(transactionId)
}

async listAllPendingTransactions(): Promise<Array<Transaction>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is used by signing worker and I think it could break something there in condition that compares refreshedTx.status !== 'pending', or at least result in unnecessary processing txes that are not pending-signature. That module will probably need to adjust to new status.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've modified the signingWorker to now look at refreshedTx.status !== 'pending' && refreshedTx.status !== 'awaiting-signature' and modified the signAndExecute (updated the doc at the top to explain the lifecycle/what happens on each tick)

Comment thread wallet-gateway/remote/src/user-api/controller.ts Outdated
Comment thread core/wallet-store-sql/src/store-sql.ts
Comment thread api-specs/openrpc-user-api.json Outdated
Comment thread wallet-gateway/remote/src/web/frontend/approve/index.ts
}
if (opts?.expectedStatus && existing.status !== opts.expectedStatus) {
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

whats the idea behind expectedStatus / how will it be used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's currently used in the applySigningResult

            const applied = await this.store.setTransactionSigned(
                tx.id,
                now,
                signingResult.txId,
                { expectedStatus: tx.status }
            )

            if (!applied) {
                const current = await this.store.getTransaction(tx.id)
                return { status: current!.status }
            }


and I think this was when I was trying tackle the scenario where someone hits approve twice on the execute where the version approve click has already gone through the sign/execute flow but then another one is in flight so this would prevent the setTransactionSigned row from matching (since the status would be executed) and then the UI shows the transaction as completed

Comment thread wallet-gateway/remote/src/ledger/transaction-service.ts Outdated
Comment thread wallet-gateway/remote/src/user-api/controller.ts Outdated
@rukmini-basu-da
rukmini-basu-da force-pushed the rukmini/async-approval branch 3 times, most recently from c8ad1b6 to f8da20f Compare September 8, 2026 14:01
@rukmini-basu-da
rukmini-basu-da force-pushed the rukmini/async-approval branch 3 times, most recently from a736ab7 to baf5639 Compare September 9, 2026 18:10
@rukmini-basu-da
rukmini-basu-da force-pushed the rukmini/async-approval branch 2 times, most recently from 048d1e3 to ef34ac9 Compare September 9, 2026 21:49
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
@rukmini-basu-da
rukmini-basu-da marked this pull request as ready for review September 10, 2026 13:17
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Comment thread api-specs/openrpc-user-api.json
// await this.expectActivityWithStatus(
// commandId,
// 'awaiting-signature'
// )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe you can have it wait until its no longer pending?

)
if (refreshed.status === 'awaiting-signature') {
return {
status: 'pending',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why status pending and not awaiting-signature?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, this is a bit weird because this value goes on as SignResult and the api spec has SignResultPending (where the status is a const: pending), so this would fail the schema validation. Kinda related to #2381 (comment)

I eventually wanted to add the SigningResultAwaitingSignature but figured that could happen in a follow up pr, but maybe it makes sense to add now. wdyt?

Comment thread wallet-gateway/remote/src/ledger/transaction-service.test.ts Outdated
Comment on lines +395 to +401
// dApp reports pending for anything not yet signed
// awaiting-signature can be a gateway UI internal distinction for polling
this.notifier.emit('txChanged', {
...tx,
status: status === 'awaiting-signature' ? 'pending' : status,
externalTxId: signingResult.txId,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont quite see a reason to not send awaiting-signature to the dapp as well, but maybe add a TODO + tech debt issue for now

Comment on lines -300 to -311
if (tx.externalTxId) {
signingResult = await driver
.getTransaction({
userId,
txId: tx.externalTxId,
})
.then(handleSigningError)
} else {
signingResult = await driver
.signTransaction(signTransactionParams)
.then(handleSigningError)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why was this removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved the path to polling the provider for the signature to refreshTransaction. And also, signWithDriver is only called from sign now and sign calls loadPreparedTransactionForSigning first so any tx with an externalTxId throws before signWithDriver runs so the if(tx.externalTxId) would never be reachable

debug: { signingResult, tx },
})

await this.applySigningResult(tx, signingResult)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need to handle the return value? I can't tell if the if statement should check signingResult.status or appliedSigningResult.status (or if it matters)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good call, this guard should help if another request tries to sign or execute the same transaction concurrently (which ideally should be prevented by loadPreparedTransactionForSigning), but it's worth having to make sure the applied result and driver result agree on the status

Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Signed-off-by: rukmini-basu-da <rukmini.basu@digitalasset.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants