Skip to content

Commit 435006a

Browse files
authored
app UI cleanup, less flaky onboarding specs (#471)
* app: backup screen UI cleanup * fix activity page filtering * fix lint tiltfile * much less flaky onboarding * more flakiness reduction * add markdown playwright report * fix playright markdown report * fix report markdown * try this way * this is the way * fix permission issue
1 parent 8cb594b commit 435006a

File tree

16 files changed

+326
-113
lines changed

16 files changed

+326
-113
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ jobs:
109109
ANVIL_BASE_FORK_URL: ${{ secrets.CI_ANVIL_BASE_FORK_URL }}
110110
YARN_ENABLE_HARDENED_MODE: 0
111111

112+
# for writing PR comments
113+
permissions:
114+
pull-requests: write
115+
112116
steps:
113117
- uses: actions/checkout@v4
114118
with:
@@ -184,6 +188,24 @@ jobs:
184188
fi
185189
done
186190
yarn playwright test
191+
- name: Playwright Markdown Report
192+
if: always() && steps.playwright.outcome != 'skipped'
193+
id: playwright-md-report
194+
shell: bash
195+
run: |
196+
echo "Markdown report:"
197+
echo "------------------"
198+
bun run packages/playwright/bin/report-markdown.ts > playwright-report.md
199+
cat playwright-report.md
200+
echo "------------------"
201+
echo "Markdown report end"
202+
- uses: mshick/add-pr-comment@v2
203+
if: always() && steps.playwright.outcome != 'skipped'
204+
with:
205+
message-id: playwright-report
206+
refresh-message-position: true
207+
message-path: |
208+
playwright-report.md
187209
- name: Tilt Down
188210
# always run tilt down if tilt ci started
189211
if: always() && steps.playwright.outcome != 'skipped'

Tiltfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ local_resource(
4040
"yarn lint",
4141
allow_parallel = True,
4242
labels = labels,
43+
resource_deps = [
44+
"yarn:install",
45+
"contracts:build",
46+
"ui:build",
47+
],
4348
)
4449

4550
cmd_button(

packages/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@wagmi/core": "^2.10.5",
1616
"app": "workspace:*",
1717
"debug": "^4.3.4",
18+
"p-queue": "^8.0.1",
1819
"superjson": "^1.13.1",
1920
"viem": "^2.13.7",
2021
"zod": "^3.22.4"

packages/api/src/routers/sendAccount.ts

Lines changed: 125 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
baseMainnet,
33
baseMainnetClient,
4-
config,
54
sendAccountFactoryAbi,
65
sendAccountFactoryAddress,
76
sendTokenAbi,
@@ -10,22 +9,25 @@ import {
109
} from '@my/wagmi'
1110
import { base16 } from '@scure/base'
1211
import { TRPCError } from '@trpc/server'
13-
import { waitForTransactionReceipt } from '@wagmi/core'
1412
import { base16Regex } from 'app/utils/base16Regex'
13+
import { hexToBytea } from 'app/utils/hexToBytea'
1514
import { COSEECDHAtoXY } from 'app/utils/passkeys'
16-
import { USEROP_SALT, getSendAccountCreateArgs, entrypoint } from 'app/utils/userop'
15+
import { supabaseAdmin } from 'app/utils/supabase/admin'
16+
import { throwIf } from 'app/utils/throwIf'
17+
import { USEROP_SALT, entrypoint, getSendAccountCreateArgs } from 'app/utils/userop'
1718
import debug from 'debug'
19+
import PQueue from 'p-queue'
1820
import { getSenderAddress } from 'permissionless'
1921
import {
2022
concat,
2123
createWalletClient,
2224
encodeFunctionData,
25+
getAbiItem,
2326
http,
2427
maxUint256,
25-
zeroAddress,
2628
publicActions,
27-
getAbiItem,
2829
withRetry,
30+
zeroAddress,
2931
} from 'viem'
3032
import { privateKeyToAccount } from 'viem/accounts'
3133
import { z } from 'zod'
@@ -43,6 +45,9 @@ const sendAccountFactoryClient = createWalletClient({
4345
transport: http(baseMainnetClient.transport.url),
4446
}).extend(publicActions)
4547

48+
// nonce storage to avoid nonce conflicts
49+
const nonceQueue = new PQueue({ concurrency: 1 })
50+
4651
export const sendAccountRouter = createTRPCRouter({
4752
create: protectedProcedure
4853
.input(
@@ -141,68 +146,85 @@ export const sendAccountRouter = createTRPCRouter({
141146
})
142147
}
143148

144-
// @todo ensure address is not already deployed
145-
// throw new TRPCError({
146-
// code: 'INTERNAL_SERVER_ERROR',
147-
// message: 'Address already deployed',
148-
// })
149+
const contractDeployed = await sendAccountFactoryClient.getBytecode({
150+
address: senderAddress,
151+
})
152+
log('createAccount', 'contractDeployed', contractDeployed)
153+
if (contractDeployed) {
154+
throw new TRPCError({
155+
code: 'INTERNAL_SERVER_ERROR',
156+
message: 'Address already deployed',
157+
})
158+
}
149159

150-
await withRetry(
160+
const initCalls = [
161+
// approve USDC to paymaster
162+
{
163+
dest: usdcAddress[baseMainnetClient.chain.id],
164+
value: 0n,
165+
data: encodeFunctionData({
166+
abi: sendTokenAbi,
167+
functionName: 'approve',
168+
args: [tokenPaymasterAddress[baseMainnetClient.chain.id], maxUint256],
169+
}),
170+
},
171+
]
172+
173+
const { request } = await sendAccountFactoryClient
174+
.simulateContract({
175+
address: sendAccountFactoryAddress[sendAccountFactoryClient.chain.id],
176+
abi: sendAccountFactoryAbi,
177+
functionName: 'createAccount',
178+
args: [
179+
keySlot, // key slot
180+
xyPubKey, // public key
181+
initCalls, // init calls
182+
USEROP_SALT, // salt
183+
],
184+
value: 0n,
185+
})
186+
.catch((e) => {
187+
log('createAccount', 'simulateContract', e)
188+
throw e
189+
})
190+
191+
const hash = await withRetry(
151192
async function createAccount() {
152-
log('createAccount', 'start')
153-
const initCalls = [
154-
// approve USDC to paymaster
155-
{
156-
dest: usdcAddress[baseMainnetClient.chain.id],
157-
value: 0n,
158-
data: encodeFunctionData({
159-
abi: sendTokenAbi,
160-
functionName: 'approve',
161-
args: [tokenPaymasterAddress[baseMainnetClient.chain.id], maxUint256],
162-
}),
163-
},
164-
]
165-
166-
const { request } = await sendAccountFactoryClient
167-
.simulateContract({
168-
address: sendAccountFactoryAddress[sendAccountFactoryClient.chain.id],
169-
abi: sendAccountFactoryAbi,
170-
functionName: 'createAccount',
171-
args: [
172-
keySlot, // key slot
173-
xyPubKey, // public key
174-
initCalls, // init calls
175-
USEROP_SALT, // salt
176-
],
177-
value: 0n,
178-
})
179-
.catch((e) => {
180-
log('createAccount', 'simulateContract', e)
181-
throw e
182-
})
183-
184-
log('createAccount', 'tx request')
185-
186-
const hash = await sendAccountFactoryClient.writeContract(request).catch((e) => {
187-
log('createAccount', 'writeContract', e)
188-
throw e
193+
const hash = await nonceQueue.add(async () => {
194+
log('createAccount queue', 'start')
195+
const nonce = await sendAccountFactoryClient
196+
.getTransactionCount({
197+
address: account.address,
198+
blockTag: 'pending',
199+
})
200+
.catch((e) => {
201+
log('createAccount queue', 'getTransactionCount', e)
202+
throw e
203+
})
204+
log('createAccount queue', 'tx request', `nonce=${nonce}`)
205+
const hash = await sendAccountFactoryClient
206+
.writeContract({
207+
...request,
208+
nonce: nonce,
209+
})
210+
.catch((e) => {
211+
log('createAccount queue', 'writeContract', e)
212+
throw e
213+
})
214+
215+
log('createAccount queue', `hash=${hash}`)
216+
return hash
189217
})
190218

191219
log('createAccount', `hash=${hash}`)
192220

193-
await waitForTransactionReceipt(config, {
194-
chainId: baseMainnetClient.chain.id,
195-
hash,
196-
}).catch((e) => {
197-
log('createAccount', 'waitForTransactionReceipt', e)
198-
throw e
199-
})
221+
return hash
200222
},
201223
{
202224
retryCount: 20,
203225
delay: ({ count, error }) => {
204226
const backoff = 500 + Math.random() * 100 // add some randomness to the backoff
205-
log(`delay count=${count} backoff=${backoff} error=${error}`)
227+
log(`createAccount delay count=${count} backoff=${backoff} error=${error}`)
206228
return backoff
207229
},
208230
shouldRetry({ count, error }) {
@@ -215,10 +237,56 @@ export const sendAccountRouter = createTRPCRouter({
215237
},
216238
}
217239
).catch((e) => {
218-
log('waitForTransactionReceipt', e)
240+
log('createAccount failed', e)
241+
console.error('createAccount failed', e)
219242
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: e.message })
220243
})
221244

245+
if (!hash) {
246+
log('createAccount', 'hash is null')
247+
throw new TRPCError({
248+
code: 'INTERNAL_SERVER_ERROR',
249+
message: 'Failed to create send account',
250+
})
251+
}
252+
253+
await withRetry(
254+
async function waitForTransactionReceipt() {
255+
const { count, error } = await supabaseAdmin
256+
.from('send_account_created')
257+
.select('*', { count: 'exact', head: true })
258+
.eq('tx_hash', hexToBytea(hash as `0x${string}`))
259+
.eq('account', hexToBytea(senderAddress))
260+
.single()
261+
throwIf(error)
262+
log('waitForTransactionReceipt', `count=${count}`)
263+
return count
264+
},
265+
{
266+
retryCount: 20,
267+
delay: ({ count, error }) => {
268+
const backoff = 500 + Math.random() * 100 // add some randomness to the backoff
269+
log(`waitForTransactionReceipt delay count=${count} backoff=${backoff}`, error)
270+
return backoff
271+
},
272+
shouldRetry({ count, error }) {
273+
// @todo handle other errors like balance not enough, invalid nonce, etc
274+
console.error('waitForTransactionReceipt failed', count, error)
275+
if (error.message.includes('Failed to create send account')) {
276+
return false
277+
}
278+
return true
279+
},
280+
}
281+
).catch((e) => {
282+
log('waitForTransactionReceipt failed', e)
283+
console.error('waitForTransactionReceipt failed', e)
284+
throw new TRPCError({
285+
code: 'INTERNAL_SERVER_ERROR',
286+
message: e.message ?? 'Failed waiting for transaction receipt',
287+
})
288+
})
289+
222290
const { error } = await supabase.rpc('create_send_account', {
223291
send_account: {
224292
address: senderAddress,

packages/app/features/account/settings/backup/confirm.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ const AddSignerButton = ({ webauthnCred }: { webauthnCred: Tables<'webauthn_cred
195195
const {
196196
receipt: { transactionHash },
197197
} = await sendUserOp({ userOp })
198-
toast.show(`Sent user op ${transactionHash}!`)
198+
console.log('sent user op', transactionHash)
199+
toast.show('Success!')
199200
router.replace('/account/settings/backup')
200201
} catch (e) {
201202
console.error(e)
@@ -238,7 +239,7 @@ const AddSignerButton = ({ webauthnCred }: { webauthnCred: Tables<'webauthn_cred
238239
{() => (
239240
<>
240241
{form.formState.errors?.root?.message ? (
241-
<Paragraph size={'$6'} fontWeight={'300'} color={'$color05'}>
242+
<Paragraph size={'$6'} fontWeight={'300'} color={'$error'}>
242243
{form.formState.errors?.root?.message}
243244
</Paragraph>
244245
) : null}

packages/app/features/account/settings/layout.web.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type SettingsLayoutProps = {
1414

1515
export const SettingsLayout = ({ children }: SettingsLayoutProps) => {
1616
return (
17-
<XStack f={1} pt={'$4'}>
17+
<XStack f={1} pt={'$4'} pb={'$6'}>
1818
<YStack
1919
backgroundColor="$color1"
2020
// this file is web-only so we can safely use CSS
@@ -34,7 +34,7 @@ export const SettingsLayout = ({ children }: SettingsLayoutProps) => {
3434
</YStack>
3535
</YStack>
3636
<Separator display="none" $gtLg={{ display: 'flex' }} vertical />
37-
<YStack f={1} ai="center" ml="$8">
37+
<YStack f={1} ai="center" $gtLg={{ ml: '$8' }}>
3838
<YStack width="100%">{children}</YStack>
3939
</YStack>
4040
</XStack>

packages/app/features/activity/utils/useActivityFeed.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import { throwIf } from 'app/utils/throwIf'
1111
import { EventArraySchema, type Activity } from 'app/utils/zod/activity'
1212
import type { ZodError } from 'zod'
1313

14-
const pgPaymasterCondValues = Object.values(tokenPaymasterAddress)
15-
.map((a) => hexToBytea(a))
16-
.join(',')
17-
1814
/**
1915
* Infinite query to fetch activity feed. Filters out activities with no from or to user (not a send app user).
2016
* @param pageSize - number of items to fetch per page
@@ -27,16 +23,23 @@ export function useActivityFeed({
2723
> {
2824
const supabase = useSupabase()
2925
async function fetchActivityFeed({ pageParam }: { pageParam: number }): Promise<Activity[]> {
26+
const pgPaymasterCondValues = Object.values(tokenPaymasterAddress)
27+
.map((a) => `${hexToBytea(a)}`)
28+
.join(',')
29+
3030
const from = pageParam * pageSize
3131
const to = (pageParam + 1) * pageSize - 1
32-
const { data, error } = await supabase
32+
const request = supabase
3333
.from('activity_feed')
3434
.select('*')
3535
.or('from_user.not.is.null, to_user.not.is.null') // only show activities with a send app user
36-
.not('data->>t', 'in', `(${pgPaymasterCondValues})`) // filter out paymaster fees for gas
37-
.not('data->>f', 'in', `(${pgPaymasterCondValues})`) // filter out paymaster refunds
36+
.or(
37+
// filter out paymaster fees for gas
38+
`data->t.is.null, data->f.is.null, and(data->>t.not.in.(${pgPaymasterCondValues}), data->>f.not.in.(${pgPaymasterCondValues}))`
39+
)
3840
.order('created_at', { ascending: false })
3941
.range(from, to)
42+
const { data, error } = await request
4043
throwIf(error)
4144
return EventArraySchema.parse(data)
4245
}

packages/app/utils/activity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ export function userNameFromActivityUser(
173173
return user.name
174174
case !!user?.send_id:
175175
return `#${user.send_id}`
176+
case user === null:
177+
return ''
176178
default:
177179
console.error('no user name found', user)
178180
if (__DEV__) {

0 commit comments

Comments
 (0)