Skip to content

Commit

Permalink
feat: add subintent polling (#289)
Browse files Browse the repository at this point in the history
Add `PreauthorizationPollingModule` which will constantly loop over unfinalized subintents and get their status from gateway
  • Loading branch information
dawidsowardx authored Dec 4, 2024
1 parent 0458176 commit 510127a
Show file tree
Hide file tree
Showing 20 changed files with 514 additions and 136 deletions.
18 changes: 14 additions & 4 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,23 @@ export type RadixButtonMode = keyof typeof RadixButtonMode
export type PersonaData = { field: string; value: string }

export const RequestStatus = {
fail: 'fail',
ignored: 'ignored',
pending: 'pending',
success: 'success',
fail: 'fail',
timedOut: 'timedOut',
cancelled: 'cancelled',
ignored: 'ignored',
/**
* Pending commit status is for preauthorization which was signed but not yet successfully committed to the network
*/
pendingCommit: 'pendingCommit',
} as const

export const RequestItemType = {
loginRequest: 'loginRequest',
dataRequest: 'dataRequest',
sendTransaction: 'sendTransaction',
proofRequest: 'proofRequest',
loginRequest: 'loginRequest',
sendTransaction: 'sendTransaction',
preAuthorizationRequest: 'preAuthorizationRequest',
} as const

Expand All @@ -49,6 +54,10 @@ export type RequestItemTypes = keyof typeof RequestItemType

export type RequestStatusTypes = keyof typeof RequestStatus

/**
* Not used in the codebase. Will be removed in the next major release future.
* @deprecated
*/
export type WalletRequest<
RequestType extends RequestItemTypes,
Status extends RequestStatusTypes,
Expand Down Expand Up @@ -81,3 +90,4 @@ export type RequestItem = {
metadata?: Record<string, string | number | boolean>
walletData?: any
}

Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const Requests: Story = {
console.log('onIgnoreTransactionItem', event)
}}
?showCancel="${args.showCancel}"
transactionIntentHash="${args.transactionIntentHash}"
hash="${args.transactionIntentHash}"
></radix-request-card>
</radix-popover>
`,
Expand Down
85 changes: 63 additions & 22 deletions packages/connect-button/src/components/card/request-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class RadixRequestCard extends LitElement {
@property({
type: String,
})
transactionIntentHash: string = ''
hash: string = ''

render() {
const icon = this.getIconFromStatus()
Expand All @@ -57,8 +57,10 @@ export class RadixRequestCard extends LitElement {
cancelled: 'Transaction Cancelled',
ignored: 'Transaction Ignored',
success: 'Send transaction',
pendingCommit: '',
timedOut: '',
content: html`
${this.renderTxIntentHash()}
${this.renderHash()}
${this.status === 'pending'
? html`<div class="request-content">
Open your Radix Wallet app to review the transaction
Expand All @@ -73,11 +75,41 @@ export class RadixRequestCard extends LitElement {
: ''}
`,
},
preAuthorizationRequest: {
pending: 'Preauthorization Pending',
fail: 'Preauthorization Failed',
cancelled: 'Preauthorization Cancelled',
success: 'Preauthorization Request',
ignored: 'Preauthorization Ignored',
pendingCommit: 'Preauthorization Lookup',
timedOut: 'Preauthorization Timed Out',
content: html`
${this.renderHash()}
${this.status === 'pending'
? html`<div class="request-content">
Open your Radix Wallet app to review the preauthorization
${this.showCancel
? html`<div class="cancel" @click=${this.onCancel}>
Cancel
</div>`
: html`<div class="cancel" @click=${this.onIgnore}>
Ignore
</div>`}
</div>`
: this.status === RequestStatus.pendingCommit
? html`<div class="request-content">
<div class="cancel" @click=${this.onIgnore}>Ignore</div>
</div>`
: ''}
`,
},
dataRequest: {
pending: 'Data Request Pending',
fail: 'Data Request Rejected',
cancelled: 'Data Request Rejected',
ignored: '',
pendingCommit: '',
timedOut: '',
success: 'Data Request',
content: this.getRequestContentTemplate(
'Open Your Radix Wallet App to complete the request',
Expand All @@ -89,6 +121,8 @@ export class RadixRequestCard extends LitElement {
cancelled: 'Login Request Rejected',
success: 'Login Request',
ignored: '',
pendingCommit: '',
timedOut: '',
content: this.getRequestContentTemplate(
'Open Your Radix Wallet App to complete the request',
),
Expand All @@ -99,16 +133,8 @@ export class RadixRequestCard extends LitElement {
cancelled: 'Proof Request Rejected',
success: 'Proof Request',
ignored: '',
content: this.getRequestContentTemplate(
'Open Your Radix Wallet App to complete the request',
),
},
preAuthorizationRequest: {
pending: 'Preauthorization Request Pending',
fail: 'Preauthorization Request Rejected',
cancelled: 'Preauthorization Request Rejected',
success: 'Preauthorization Request',
ignored: '',
pendingCommit: '',
timedOut: '',
content: this.getRequestContentTemplate(
'Open Your Radix Wallet App to complete the request',
),
Expand Down Expand Up @@ -136,7 +162,7 @@ export class RadixRequestCard extends LitElement {
: ''
}

private isErrorStatus(status: RequestStatusTypes) {
private hasErrorIcon(status: RequestStatusTypes) {
return (
[
RequestStatus.cancelled,
Expand All @@ -146,19 +172,31 @@ export class RadixRequestCard extends LitElement {
).includes(status)
}

private hasPendingIcon(status: RequestStatusTypes) {
return ([RequestStatus.pending, RequestStatus.pendingCommit] as string[]).includes(
status,
)
}

private hasIgnoredIcon(status: RequestStatusTypes) {
return (
[RequestStatus.ignored, RequestStatus.timedOut] as string[]
).includes(status)
}

private getIconFromStatus() {
return this.status === RequestStatus.pending
return this.hasPendingIcon(this.status)
? 'pending'
: this.status === RequestStatus.ignored
: this.hasIgnoredIcon(this.status)
? 'ignored'
: this.isErrorStatus(this.status)
: this.hasErrorIcon(this.status)
? 'error'
: 'checked'
}

private getStylingFromStatus() {
return classMap({
dimmed: this.isErrorStatus(this.status),
dimmed: this.hasErrorIcon(this.status),
inverted: this.status === 'pending',
})
}
Expand Down Expand Up @@ -189,21 +227,24 @@ export class RadixRequestCard extends LitElement {
)
}

private renderTxIntentHash() {
return this.transactionIntentHash
private renderHash() {
return this.hash
? html`<div class="transaction">
<span class="text-dimmed">ID:</span>
<radix-link
displayText="${shortenAddress(this.transactionIntentHash)}"
displayText="${shortenAddress(this.hash)}"
@click=${(event: MouseEvent) => {
event.preventDefault()
this.dispatchEvent(
new CustomEvent('onLinkClick', {
bubbles: true,
composed: true,
detail: {
type: 'transaction',
data: this.transactionIntentHash,
type:
this.type === 'sendTransaction'
? 'transaction'
: 'subintent',
data: this.hash,
},
}),
)
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-button/src/components/pages/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class RadixRequestsPage extends LitElement {
type="${requestItem.type}"
status="${requestItem.status}"
id="${requestItem.interactionId}"
transactionIntentHash="${requestItem.transactionIntentHash || ''}"
hash="${requestItem.transactionIntentHash || ''}"
?showCancel="${requestItem.showCancel}"
timestamp=${requestItem.createdAt}
></radix-request-card>`,
Expand Down
1 change: 1 addition & 0 deletions packages/dapp-toolkit/src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type Providers = {
export type ExplorerConfig = {
baseUrl: string
transactionPath: string
subintentPath: string
accountsPath: string
}

Expand Down
79 changes: 79 additions & 0 deletions packages/dapp-toolkit/src/helpers/exponential-backoff.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { afterAll, describe, expect, it, vi } from 'vitest'
import { ExponentialBackoff } from './exponential-backoff'
import { delayAsync } from '../test-helpers/delay-async'
import { Subscription } from 'rxjs'

describe('exponential backoff', () => {
const subscription = new Subscription()

it('should emit withBackoff$ observable', async () => {
const backoff = ExponentialBackoff({
maxDelayTime: 2000,
multiplier: 2,
interval: 1000,
})

const spy = vi.fn()

subscription.add(
backoff.withBackoff$.subscribe(() => {
spy()
backoff.trigger.next()
}),
)

await delayAsync(4500)

expect(spy).toHaveBeenCalledTimes(3)
})

it('should emit error after timeout', async () => {
const backoff = ExponentialBackoff({
maxDelayTime: 2000,
multiplier: 2,
interval: 2000,
timeout: new Date(Date.now() + 1000),
})
const spy = vi.fn()

subscription.add(
backoff.withBackoff$.subscribe((res) => {
spy(res.isOk() ? res.value : res.error)

backoff.trigger.next()
}),
)

await delayAsync(2000)

expect(spy).toHaveBeenCalledWith(0)
expect(spy).toHaveBeenCalledWith({
error: 'timeout',
})
})

it('should emit error after stop', async () => {
const backoff = ExponentialBackoff({})
const spy = vi.fn()

subscription.add(
backoff.withBackoff$.subscribe((res) => {
spy(res.isOk() ? res.value : res.error)
if (res.isOk()) {
backoff.trigger.next()
}
}),
)

await delayAsync(2000)
backoff.stop()
expect(spy).toHaveBeenCalledWith(0)
expect(spy).toHaveBeenCalledWith({
error: 'stopped',
})
})

afterAll(() => {
subscription.unsubscribe()
})
})
24 changes: 16 additions & 8 deletions packages/dapp-toolkit/src/helpers/exponential-backoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { map, merge, of, Subject, switchMap, timer } from 'rxjs'
export type ExponentialBackoffInput = {
multiplier?: number
maxDelayTime?: number
timeout?: number
timeout?: number | Date
interval?: number
}
export type ExponentialBackoff = typeof ExponentialBackoff
Expand All @@ -17,6 +17,7 @@ export const ExponentialBackoff = ({
interval = 2_000,
}: ExponentialBackoffInput = {}) => {
const trigger = new Subject<void>()
const stop = new Subject<void>()
let numberOfRetries = 0

const backoff$ = merge(
Expand All @@ -36,12 +37,19 @@ export const ExponentialBackoff = ({
)

const withBackoffAndTimeout$: Observable<Result<number, { error: string }>> =
timeout
? merge(
backoff$,
timer(timeout).pipe(map(() => err({ error: 'timeout' }))),
)
: backoff$
merge(
stop.asObservable().pipe(map(() => err({ error: 'stopped' }))),
timeout
? merge(
backoff$,
timer(timeout).pipe(map(() => err({ error: 'timeout' }))),
)
: backoff$,
)

return { trigger, withBackoff$: withBackoffAndTimeout$ }
return {
trigger,
withBackoff$: withBackoffAndTimeout$,
stop: () => stop.next(),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export const ConnectButtonModule = (
const logger = input?.logger?.getSubLogger({ name: 'ConnectButtonModule' })
const subjects = input.subjects || ConnectButtonSubjects()
const dAppDefinitionAddress = input.dAppDefinitionAddress
const { baseUrl, accountsPath, transactionPath } = input.explorer ?? {
baseUrl: RadixNetworkConfigById[input.networkId].dashboardUrl,
transactionPath: '/transaction/',
accountsPath: '/account/',
}
const { baseUrl, accountsPath, transactionPath, subintentPath } =
input.explorer ?? {
baseUrl: RadixNetworkConfigById[input.networkId].dashboardUrl,
transactionPath: '/transaction/',
subintentPath: '/subintent/',
accountsPath: '/account/',
}
const statusStorage = input.providers.storageModule

const stateModule = input.providers.stateModule
Expand Down Expand Up @@ -287,11 +289,15 @@ export const ConnectButtonModule = (
subjects.onLinkClick
.pipe(
tap(({ type, data }) => {
if (['account', 'transaction'].includes(type)) {
if (['account', 'transaction', 'subintent'].includes(type)) {
if (!baseUrl || !window) return

const url = `${baseUrl}${
type === 'transaction' ? transactionPath : accountsPath
type === 'transaction'
? transactionPath
: type === 'subintent'
? subintentPath
: accountsPath
}${data}`

window.open(url)
Expand Down
Loading

0 comments on commit 510127a

Please sign in to comment.