Skip to content

Commit b7dd9ed

Browse files
authored
sendtag checkout referral rewards (#615)
* shovel: update config to include URLs * contracts: sendtag checkout emit receipts * e2e: playwright can import esm only now * app: handle already expired userop error * wagmi: fixup types * yarn lint after unit tests * app: update sendtag checkout This commit marks the first part of transitioning our Sendtag registration process from Ethereum to USDC. It includes the following changes: - Replace ETH pricing with USDC pricing for Sendtags - Introduce new pricing structure and referral bonus system - Update UI components to reflect new USDC pricing - Refactor pricing logic into a separate module for better maintainability New Pricing Structure (Registration Fee / Referral Bonus): - 6+ characters: 7 USDC / 2 USDC - 5 characters: 15 USDC / 4 USDC - 4 characters: 30 USDC / 8 USDC - 1-3 characters: 60 USDC / 16 USDC Key changes: 1. Created new file 'app/data/sendtags.ts' to centralize pricing logic 2. Updated 'tagRouter' in 'api/src/routers/tag.ts' to use new pricing 3. Modified UI components to display USDC prices instead of ETH 4. Updated tests to reflect new USDC pricing * supabase: sendtag checkout updates - confirm_tags to use transfers to sendtag checkout contract - update tag receipt activity - updates tests * e2e: update confirm tag * update sendtag referrer for checkout * e2e: update refer a tag * app: show sendtag referral rewards activity * clean up types * update shovel config * e2e: more playwright cleanup * supabase sendtag checkout updates * use sendtag_checkout_receipts for confirming tags verify referrer and reward updates sendtag checkout contract address * supabase: remove sendtag checkout contract table * supabase: update confirm_tags to use sendtag_checkout_receipts * contracts: handle duplicate deploys on sendtag_checkout_receipts * fixup activity spec * playwright: cleanup home onboarded spec * snapshot update * e2e fixes * lots of little tweaks and polish * handle when not enough funds * use sendtag for referrer * rename fetchSendtagCheckoutTransfers to receipts * fix referrer check logic * simplify referrer check * naming * add assertions on checkout receipts * more renaming * add userop throwNiceError * fix lints, cleanup, increase dbdev timeouts * upgrade shovel ts config * total price test * add app/data/sendtags test * little more cleanup * invalidate queries * fix lint error
1 parent b94cf76 commit b7dd9ed

File tree

82 files changed

+2713
-1114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+2713
-1114
lines changed

.github/workflows/ci.yml

+2-20
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,6 @@ jobs:
3131
echo github.ref=${{ github.ref }}
3232
echo github.head_ref=${{ github.head_ref }}
3333
34-
lint:
35-
runs-on: self-hosted
36-
needs: [cache-deps]
37-
env:
38-
YARN_ENABLE_HARDENED_MODE: 0
39-
40-
steps:
41-
- uses: actions/checkout@v4
42-
with:
43-
submodules: recursive
44-
- name: Setup Environment
45-
uses: ./.github/actions/setup-env
46-
env:
47-
YARN_ENABLE_HARDENED_MODE: 0
48-
SKIP_YARN_POST_INSTALL: 1
49-
- name: Build
50-
run: yarn build
51-
- name: Lint
52-
run: yarn lint
53-
5434
unit-tests:
5535
name: Unit Tests
5636
runs-on: self-hosted
@@ -94,6 +74,8 @@ jobs:
9474
id: tilt-ci
9575
shell: bash
9676
run: tilt ci unit-tests --timeout=10m
77+
- name: Lint
78+
run: yarn lint
9779
- name: Tilt Down
9880
# always run tilt down if tilt ci started
9981
if: always() && steps.tilt-ci.outcome != 'skipped'

.snaplet/snaplet-client.d.ts

+32
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,27 @@ type Override = {
394394
event_id?: string;
395395
};
396396
}
397+
sendtag_checkout_receipts?: {
398+
name?: string;
399+
fields?: {
400+
id?: string;
401+
event_id?: string;
402+
chain_id?: string;
403+
log_addr?: string;
404+
block_time?: string;
405+
tx_hash?: string;
406+
sender?: string;
407+
amount?: string;
408+
referrer?: string;
409+
reward?: string;
410+
ig_name?: string;
411+
src_name?: string;
412+
block_num?: string;
413+
tx_idx?: string;
414+
log_idx?: string;
415+
abi_idx?: string;
416+
};
417+
}
397418
tag_receipts?: {
398419
name?: string;
399420
fields?: {
@@ -715,6 +736,17 @@ export interface Fingerprint {
715736
logIdx?: FingerprintNumberField;
716737
abiIdx?: FingerprintNumberField;
717738
}
739+
sendtagCheckoutReceipts?: {
740+
id?: FingerprintNumberField;
741+
chainId?: FingerprintNumberField;
742+
blockTime?: FingerprintNumberField;
743+
amount?: FingerprintNumberField;
744+
reward?: FingerprintNumberField;
745+
blockNum?: FingerprintNumberField;
746+
txIdx?: FingerprintNumberField;
747+
logIdx?: FingerprintNumberField;
748+
abiIdx?: FingerprintNumberField;
749+
}
718750
tagReceipts?: {
719751
id?: FingerprintNumberField;
720752
createdAt?: FingerprintDateField;

.snaplet/snaplet.d.ts

+18
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,23 @@ interface Table_public_send_token_transfers {
438438
log_idx: number;
439439
abi_idx: number;
440440
}
441+
interface Table_public_sendtag_checkout_receipts {
442+
id: number;
443+
chain_id: number;
444+
log_addr: string;
445+
block_time: number;
446+
tx_hash: string;
447+
sender: string;
448+
amount: number;
449+
referrer: string;
450+
reward: number;
451+
ig_name: string;
452+
src_name: string;
453+
block_num: number;
454+
tx_idx: number;
455+
log_idx: number;
456+
abi_idx: number;
457+
}
441458
interface Table_auth_sessions {
442459
id: string;
443460
user_id: string;
@@ -619,6 +636,7 @@ interface Schema_public {
619636
send_liquidity_pools: Table_public_send_liquidity_pools;
620637
send_revenues_safe_receives: Table_public_send_revenues_safe_receives;
621638
send_token_transfers: Table_public_send_token_transfers;
639+
sendtag_checkout_receipts: Table_public_sendtag_checkout_receipts;
622640
tag_receipts: Table_public_tag_receipts;
623641
tag_reservations: Table_public_tag_reservations;
624642
tags: Table_public_tags;

apps/distributor/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"viem": "^2.13.7"
3030
},
3131
"devDependencies": {
32-
"@types/bun": "latest",
32+
"@types/bun": "^1.1.6",
3333
"@types/express": "^4",
3434
"@types/supertest": "^2.0.16",
3535
"debug": "^4.3.5",

apps/workers/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"workflow": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/client.ts"
1313
},
1414
"devDependencies": {
15-
"@types/bun": "latest",
15+
"@types/bun": "^1.1.6",
1616
"ts-node": "^10.9.2",
1717
"typescript": "^5.5.3"
1818
},

environment.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ declare global {
4747
* if (__DEV__) console.log('Running in dev mode')
4848
*/
4949
const __DEV__: boolean
50+
interface Window {
51+
ethereum: IWeb3Provider
52+
}
5053
}
5154

5255
export type {}

packages/api/src/routers/tag.ts

+60-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import type { PostgrestError } from '@supabase/supabase-js'
22
import { TRPCError } from '@trpc/server'
3-
import { getPrice } from 'app/features/account/sendtag/checkout/checkout-utils'
3+
import { reward, total } from 'app/data/sendtags'
4+
import {
5+
fetchReferrer,
6+
fetchSendtagCheckoutReceipts,
7+
} from 'app/features/account/sendtag/checkout/checkout-utils'
8+
import { assert } from 'app/utils/assert'
49
import { hexToBytea } from 'app/utils/hexToBytea'
510
import { supabaseAdmin } from 'app/utils/supabase/admin'
11+
import { throwIf } from 'app/utils/throwIf'
612
import { byteaTxHash } from 'app/utils/zod'
713
import debug from 'debug'
814
import { withRetry } from 'viem'
@@ -23,7 +29,19 @@ export const tagRouter = createTRPCRouter({
2329
)
2430
.mutation(async ({ ctx: { supabase, referralCode }, input: { transaction: txHash } }) => {
2531
const { data: tags, error: tagsError } = await supabase.from('tags').select('*')
26-
32+
const { data: profile, error: profileError } = await supabase
33+
.from('profiles')
34+
.select('*')
35+
.single()
36+
// if profile error, return early
37+
if (profileError || !profile) {
38+
log('profile not found', profileError)
39+
throw new TRPCError({
40+
code: 'INTERNAL_SERVER_ERROR',
41+
message: profileError.message || 'Profile not found',
42+
})
43+
}
44+
// if tags error, return early
2745
if (tagsError) {
2846
if (tagsError.code === 'PGRST116') {
2947
log('no tags to confirm')
@@ -32,15 +50,38 @@ export const tagRouter = createTRPCRouter({
3250
message: 'No tags to confirm.',
3351
})
3452
}
53+
log('tags error', tagsError)
3554
throw new TRPCError({
3655
code: 'INTERNAL_SERVER_ERROR',
3756
message: tagsError.message,
3857
})
3958
}
4059

60+
// if referral code is present and not the same as the profile, fetch the referrer profile
61+
const referrerProfile = referralCode
62+
? await fetchReferrer({
63+
supabase,
64+
profile,
65+
referralCode,
66+
}).catch((e) => {
67+
const error = e as unknown as PostgrestError
68+
if (error.code === 'PGRST116') {
69+
return null
70+
}
71+
throw new TRPCError({
72+
code: 'INTERNAL_SERVER_ERROR',
73+
message: error.message,
74+
})
75+
})
76+
: null
77+
4178
const pendingTags = tags.filter((t) => t.status === 'pending')
42-
const ethAmount = getPrice(pendingTags)
79+
const amountDue = total(pendingTags)
4380
const txBytea = byteaTxHash.safeParse(hexToBytea(txHash as `0x${string}`))
81+
const rewardDue =
82+
referrerProfile?.address && referrerProfile.tag // ensure referrer exists and has a tag
83+
? pendingTags.reduce((acc, t) => acc + reward(t.name.length), 0n)
84+
: 0n
4485

4586
if (!txBytea.success) {
4687
log('transaction hash required')
@@ -52,16 +93,12 @@ export const tagRouter = createTRPCRouter({
5293

5394
const data = await withRetry(
5495
async () => {
55-
const { data, error } = await supabase
56-
.from('send_revenues_safe_receives')
57-
.select('*')
96+
const { data, error } = await fetchSendtagCheckoutReceipts(supabase)
5897
.eq('tx_hash', txBytea.data)
5998
.single()
60-
61-
if (error) {
62-
throw error
63-
}
64-
99+
throwIf(error)
100+
assert(!!data, 'No checkout receipt found')
101+
log('fetched checkout receipt', data)
65102
return data
66103
},
67104
{
@@ -91,33 +128,33 @@ export const tagRouter = createTRPCRouter({
91128
})
92129
})
93130

94-
const { event_id, sender: senderPgB16, v } = data
131+
const { event_id, amount, referrer, reward: rewardSentStr } = data
132+
const rewardSent = rewardSentStr ? BigInt(rewardSentStr) : 0n
95133

96-
if (!senderPgB16 || !v) {
97-
log('no sender or v found', `txHash=${txHash}`)
98-
throw new TRPCError({
99-
code: 'BAD_REQUEST',
100-
message: 'No sender or v found. Please try again.',
101-
})
102-
}
134+
const invalidAmount = !amount || BigInt(amount) !== amountDue
135+
const invalidReferrer =
136+
(!referrer && rewardSent !== 0n) || // no referrer and reward is sent
137+
(referrer && rewardSent !== rewardDue) // referrer and invalid reward
103138

104-
if (!v || BigInt(v) !== ethAmount) {
105-
log('transaction is not a payment for tags or incorrect amount', `txHash=${txHash}`)
139+
if (invalidAmount || invalidReferrer) {
140+
log('transaction is not a payment for tags or incorrect amount', `txHash=${txHash}`, data)
106141
throw new TRPCError({
107142
code: 'BAD_REQUEST',
108143
message: 'Transaction is not a payment for tags or incorrect amount.',
109144
})
110145
}
111146

147+
log('confirming tags', `event_id=${event_id}`)
148+
112149
// confirm all pending tags and save the transaction receipt
113150
const { error: confirmTagsErr } = await supabaseAdmin.rpc('confirm_tags', {
114151
tag_names: pendingTags.map((t) => t.name),
115152
event_id,
116-
referral_code_input: referralCode ?? '',
153+
referral_code_input: referrerProfile?.refcode ?? '',
117154
})
118155

119156
if (confirmTagsErr) {
120-
log('confirm tags error', confirmTagsErr)
157+
console.error('confirm tags error', confirmTagsErr)
121158
throw new TRPCError({
122159
code: 'INTERNAL_SERVER_ERROR',
123160
message: confirmTagsErr.message,

packages/app/__mocks__/@my/wagmi/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const mockMyWagmi = {
4040
entryPointAddress: {
4141
845337: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
4242
},
43+
sendtagCheckoutAddress: {
44+
845337: '0xfC1e51BBae1C1Ee9e6Cc629ea0023329EA5023a6',
45+
},
4346
sendVerifierAbi: [
4447
{
4548
type: 'function',
@@ -73,5 +76,6 @@ export const sendTokenAddress = mockMyWagmi.sendTokenAddress
7376
export const tokenPaymasterAddress = mockMyWagmi.tokenPaymasterAddress
7477
export const entryPointAddress = mockMyWagmi.entryPointAddress
7578
export const sendVerifierAbi = mockMyWagmi.sendVerifierAbi
79+
export const sendtagCheckoutAddress = mockMyWagmi.sendtagCheckoutAddress
7680
export const sendVerifierProxyAddress = mockMyWagmi.sendVerifierProxyAddress
7781
export default mockMyWagmi

packages/app/data/sendtags.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import '@jest/globals'
2+
import { reward, total } from './sendtags'
3+
4+
const decimals = 10n ** 6n
5+
6+
const tags = [
7+
{ name: '1' }, // 60 USDC (16 USDC reward)
8+
{ name: '12' }, // 60 USDC (16 USDC reward)
9+
{ name: '123' }, // 60 USDC (16 USDC reward)
10+
{ name: '1234' }, // 30 USDC (8 USDC reward)
11+
{ name: '12345' }, // 15 USDC (4 USDC reward)
12+
{ name: '123456' }, // 7 USDC (2 USDC reward)
13+
{ name: '1234567' }, // 7 USDC (2 USDC reward)
14+
]
15+
16+
const totalDue = (60n + 60n + 60n + 30n + 15n + 7n + 7n) * decimals
17+
18+
const rewardDue = (16n + 16n + 16n + 8n + 4n + 2n + 2n) * decimals
19+
20+
describe('Sendtag data', () => {
21+
it('can calculate total correctly', () => {
22+
expect(total(tags)).toEqual(totalDue)
23+
})
24+
it('can calculate reward correctly', () => {
25+
const _reward = tags.reduce((acc, tag) => {
26+
return acc + reward(tag.name.length)
27+
}, 0n)
28+
expect(_reward).toEqual(rewardDue)
29+
})
30+
})

0 commit comments

Comments
 (0)