Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(unlock-app): checkout frame errors #14912

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
37 changes: 37 additions & 0 deletions unlock-app/app/frames/checkout/approve/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Abi, encodeFunctionData } from 'viem'
import { frames } from '../frames'
import { transaction } from 'frames.js/core'
import { erc20Abi, getKeyPrice } from '../components/utils'

export const POST = frames(async (ctx) => {
if (!ctx?.message) {
throw new Error('Invalid frame message')
}

const userAddress = ctx.message.address!
const network = Number(ctx.state.lock!.network)
const lockAddress = ctx.state.lock!.address

const keyPrice = await getKeyPrice({
lockAddress,
network,
userAddress,
})

const calldata = encodeFunctionData({
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to use that! unlock-js has the logic you need to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn’t catch that, the encodeFunctionData part or the entire erc20 approve 🤔

Copy link
Member

Choose a reason for hiding this comment

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

So I think you should first check if the user has already approved enough (and you can compute the amount that needs to be approved based on the ehckout config (especially if it supports renewals)

Sorry my initial comment was unclear. Check unlock-js where there is already logic around how to compute the amount to be approved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

seen the logic in unlock-js/src/PublicLock/v10/getPurchaseKeysArguments to account for renewals and done it slightly differently.
my approach now checks if a lock is renewable to decide whether to render the approve button, so you can always approve a different amount before the purchase to replace any previous approval

abi: erc20Abi,
functionName: 'approve',
args: [lockAddress, keyPrice],
})

return transaction({
chainId: `eip155:${network}`,
method: 'eth_sendTransaction',
params: {
abi: erc20Abi as Abi,
to: ctx.state.lock?.tokenAddress as `0x${string}`,
data: calldata,
value: '0x0',
},
})
})
20 changes: 14 additions & 6 deletions unlock-app/app/frames/checkout/components/defaultFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/* eslint-disable react/jsx-key */
import React from 'react'
import { Button } from 'frames.js/next'
import { State } from '../frames'
import { DefaultImage } from './DefaultImage'

export function getDefaultFrame(state: State) {
const lock = state.lock!
export function getDefaultFrame(ctx: any) {
const lock = ctx.state.lock!
const isErc20 = !!ctx.state.lock?.tokenAddress
const approved = ctx.searchParams.approved || !isErc20
const buttonText = approved
? 'Purchase a key'
: `Approve ${lock.currencySymbol} spending`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const buttonText = approved
? 'Purchase a key'
: `Approve ${lock.currencySymbol} spending`
const buttonText = approved
? 'Mint'
: `Approve ${lock.currencySymbol} spending`


return {
image: <DefaultImage lock={lock} />,
Expand All @@ -18,10 +22,14 @@ export function getDefaultFrame(state: State) {
},
},
buttons: [
<Button action="tx" target="/txdata" post_url="?success=true">
Purchase a key
<Button
action="tx"
target={approved ? '/txdata' : '/approve'}
post_url={approved ? '?success=true' : '?approved=true'}
>
{buttonText}
</Button>,
],
state,
state: ctx.state,
}
}
61 changes: 61 additions & 0 deletions unlock-app/app/frames/checkout/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@ import { locksmith } from '../../../../src/config/locksmith'
import networks from '@unlock-protocol/networks'
import { config as appConfig } from '~/config/app'

interface GetKeyPriceParams {
lockAddress: string
network: number
userAddress: string
}

export const erc20Abi = [
{
type: 'function',
name: 'approve',
constant: false,
inputs: [
{
name: 'spender',
type: 'address',
},
{
name: 'amount',
type: 'uint256',
},
],
outputs: [
{
name: '',
type: 'bool',
},
],
stateMutability: 'nonpayable',
},
]

export async function getConfig(id: string) {
const { config } = await fetch(
`${appConfig.locksmithHost}/v2/checkout/${id}`
Expand Down Expand Up @@ -42,6 +73,16 @@ export async function getLockDataFromCheckout(id: string) {
const res = await web3Service.getLock(lockAddress, network)
const price = `${res.keyPrice} ${res.currencySymbol}`

let tokenAddress
if (res.currencySymbol !== 'ETH') {
Copy link
Member

Choose a reason for hiding this comment

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

That's not the right way to check if the lock is priced in the native currency. Thus would fail on Polygon for example. Just check that the lock.tokenAddress is the Zero address.

const tokens = networks[network].tokens
const matches = tokens!.filter(
(token) => token.symbol === res.currencySymbol
)
tokenAddress = matches.find((token) => token.featured) || matches[0] || null
tokenAddress = tokenAddress.address
Copy link
Member

Choose a reason for hiding this comment

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

That's also not correct. just use lock.tokenAddress here.

}

const redirectUri = config.redirectUri
const redirectText = config.endingCallToAction

Expand All @@ -53,9 +94,29 @@ export async function getLockDataFromCheckout(id: string) {
defaultImage,
description,
price,
currencySymbol: res.currencySymbol,
tokenAddress,
redirectUri,
redirectText,
}

return lock
}

export async function getKeyPrice({
lockAddress,
network,
userAddress,
}: GetKeyPriceParams) {
const web3Service = new Web3Service(networks)
const mydata = '0x'
let price = await web3Service.purchasePriceFor({
lockAddress,
userAddress: userAddress,
network,
data: mydata,
referrer: userAddress,
})
price = price.toString()
return price
}
2 changes: 2 additions & 0 deletions unlock-app/app/frames/checkout/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type Lock = {
defaultImage?: string
description: string
price: string
currencySymbol: string
tokenAddress?: string
redirectUri?: string
redirectText?: string
}
Expand Down
4 changes: 2 additions & 2 deletions unlock-app/app/frames/checkout/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const getInitialFrame = frames(async (ctx) => {
const lock = await getLockDataFromCheckout(id)
state.lock = lock

return getDefaultFrame(state)
return getDefaultFrame(ctx)
})

const getOtherFrames = frames(async (ctx) => {
Expand Down Expand Up @@ -46,7 +46,7 @@ const getOtherFrames = frames(async (ctx) => {
}
}

return getDefaultFrame(state)
return getDefaultFrame(ctx)
})

export const GET = getInitialFrame
Expand Down
25 changes: 7 additions & 18 deletions unlock-app/app/frames/checkout/txdata/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Abi, encodeFunctionData } from 'viem'
import { frames } from '../frames'
import { transaction } from 'frames.js/core'
import { PublicLockV14 } from '@unlock-protocol/contracts'
import { Web3Service } from '@unlock-protocol/unlock-js'
import networks from '@unlock-protocol/networks'
import { getKeyPrice } from '../components/utils'

const abi = PublicLockV14.abi

Expand All @@ -12,25 +11,15 @@ export const POST = frames(async (ctx) => {
throw new Error('Invalid frame message')
}

const userAddress = ctx.message.connectedAddress!
const userAddress = ctx.message.address!
const network = Number(ctx.state.lock!.network)
const lockAddress = ctx.state.lock!.address

async function getKeyPrice() {
const web3Service = new Web3Service(networks)
const mydata = '0x'
let price = await web3Service.purchasePriceFor({
lockAddress,
userAddress: userAddress,
network,
data: mydata,
referrer: userAddress,
})
price = price.toString()
return price
}

const keyPrice = await getKeyPrice()
const keyPrice = await getKeyPrice({
lockAddress,
network,
userAddress,
})

const calldata = encodeFunctionData({
Copy link
Member

Choose a reason for hiding this comment

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

Please use unlock'js walletService here as well so this supports any version of the lock contract!

Copy link
Contributor Author

@iMac7 iMac7 Nov 15, 2024

Choose a reason for hiding this comment

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

I ran into errors getting the abi with walletService.lockContractAbiVersion() which seems the closest method in functionality imo.
Would it work for all versions just importing the generic PublicLock without a version, or i'll need to dig into walletservice a bit more 🤔

abi,
Expand Down
Loading