Skip to content

forte payment #405

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

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
49 changes: 42 additions & 7 deletions examples/react/src/components/Connected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useOpenWalletModal } from '@0xsequence/wallet-widget'
import { CardButton, Header, WalletListItem } from 'example-shared-components'
import { AnimatePresence } from 'motion/react'
import React, { type ComponentProps, useEffect } from 'react'
import { encodeFunctionData, formatUnits, parseAbi, toHex } from 'viem'
import { encodeFunctionData, formatUnits, parseAbi, toHex, zeroAddress } from 'viem'
import { useAccount, useChainId, usePublicClient, useSendTransaction, useWalletClient, useWriteContract } from 'wagmi'

import { sponsoredContractAddresses } from '../config'
Expand Down Expand Up @@ -357,11 +357,28 @@ export const Connected = () => {
// const contractId = '674eb55a3d739107bbd18ecb'

// // ERC-20 contract
const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
const salesContractAddress = '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b'
// const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
// const salesContractAddress = '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b'
// const collectionAddress = '0xdeb398f41ccd290ee5114df7e498cf04fac916cb'
// const price = '20000'
// const contractId = '674eb5613d739107bbd18ed2'

// const chainId = 137

// Forte payment testnet testing opensea
// const currencyAddress = zeroAddress
// const salesContractAddress = '0x1130e2e03f682f05f298fd702787d9bd0bf94316'
// const collectionAddress = '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b'
// const price = '600000000000000'
// const contractId = ''
// const chainId = 11155111

// Forte payment testnet testing magiceden
const currencyAddress = zeroAddress
const salesContractAddress = '0x0000000000000068F116a894984e2DB1123eB395'
const collectionAddress = '0xdeb398f41ccd290ee5114df7e498cf04fac916cb'
const price = '20000'
const contractId = '674eb5613d739107bbd18ed2'
const price = '100000000000000'
const contractId = ''

const chainId = 137

Expand All @@ -387,6 +404,8 @@ export const Connected = () => {
]
})

const creditCardProvider = checkoutProvider || 'transak'

openSelectPaymentModal({
collectibles,
chain: chainId,
Expand All @@ -395,14 +414,30 @@ export const Connected = () => {
recipientAddress: address,
currencyAddress,
collectionAddress,
creditCardProviders: [checkoutProvider || 'transak'],
creditCardProviders: [creditCardProvider],
onRampProvider: onRampProvider ? (onRampProvider as TransactionOnRampProvider) : TransactionOnRampProvider.transak,
transakConfig: {
contractId,
apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf'
},
// forteConfig: {
// protocol: 'mint'
// },
// Config for seaport testnet
// forteConfig: {
// protocol: 'seaport',
// orderHash: '0xa29984c1892bb28bc35170a0e7e4db64ceacfbd20dc5576bd67f1aae9dd678a3',
// // listings with amount > 1 are bugged
// // orderHash: '0x832b698e52508849fe533fdef53d6d9674be4f43eb1a2eb3415e46041f087af9',
// seaportProtocolAddress: '0x0000000000000068F116a894984e2DB1123eB395',
// sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986'
// },
forteConfig: {
protocol: 'magiceden',
sellerAddress: '0xCb88b6315507e9d8c35D81AFB7F190aB6c3227C9'
},
copyrightText: 'ⓒ2024 Sequence',
onSuccess: (txnHash: string) => {
onSuccess: (txnHash?: string) => {
console.log('success!', txnHash)
},
onError: (error: Error) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/react/src/components/CustomCheckout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const CustomCheckout = () => {
contractId,
apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf'
},
onSuccess: (txnHash: string) => {
onSuccess: (txnHash?: string) => {
console.log('success!', txnHash)
},
onError: (error: Error) => {
Expand Down
4 changes: 3 additions & 1 deletion examples/react/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ export const checkoutConfig: SequenceCheckoutConfig = {
sardineCheckoutUrl: 'https://sardine-checkout-sandbox.sequence.info',
sardineOnRampUrl: 'https://crypto.sandbox.sardine.ai/',
transakApiUrl: 'https://global-stg.transak.com',
transakApiKey: 'c20f2a0e-fe6a-4133-8fa7-77e9f84edf98'
transakApiKey: 'c20f2a0e-fe6a-4133-8fa7-77e9f84edf98',
fortePaymentUrl: 'https://staging-api.pti-dev.cloud',
forteWidgetUrl: 'https://dev-forte-payments-cdn.pti-dev.cloud/forte-payments-widget.js'
}
: undefined
}
6 changes: 3 additions & 3 deletions packages/checkout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const MyComponent = () => {
collectionAddress,
creditCardProviders: ['sardine'],
copyrightText: 'ⓒ2024 Sequence',
onSuccess: (txnHash: string) => {
onSuccess: (txnHash?: string) => {
console.log('success!', txnHash)
},
onError: (error: Error) => {
Expand Down Expand Up @@ -156,7 +156,7 @@ const MyComponent = () => {
quantity: "1",
},
],
onSuccess: (txnHash: string) => {
onSuccess: (txnHash?: string) => {
console.log("success!", txnHash);
},
onError: (error: Error) => {
Expand Down Expand Up @@ -278,7 +278,7 @@ const CustomCheckoutUI = () => {
contractId,
apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf'
},
onSuccess: (txnHash: string) => {
onSuccess: (txnHash?: string) => {
console.log('success!', txnHash)
},
onError: (error: Error) => {
Expand Down
242 changes: 240 additions & 2 deletions packages/checkout/src/api/data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { SequenceAPIClient } from '@0xsequence/api'
import { TokenMetadata } from '@0xsequence/metadata'
import { ChainId, networks } from '@0xsequence/network'
import { ChainId, networks, findSupportedNetwork } from '@0xsequence/network'
import { zeroAddress } from 'viem'

import { CreditCardCheckout } from '../contexts'
import {
CreditCardCheckout,
ForteProtocolType,
ForteConfig,
ForteMintConfig,
ForteSeaportConfig,
ForteMagicedenConfig
} from '../contexts'

export interface FetchSardineClientTokenReturn {
token: string
Expand Down Expand Up @@ -233,3 +241,233 @@ export const fetchSardineOnRampLink = async ({

return url.href
}

export interface FetchForteAccessTokenReturn {
accessToken: string
expiresIn: number
tokenType: string
}

export const fetchForteAccessToken = async (forteApiUrl: string): Promise<FetchForteAccessTokenReturn> => {
const clientId = '5tpnj5869vs3jpgtpif2ci8v08'
const clientSecret = 'jpkbg3e2ho9rbd0959qe5l6ke238d4bca2nptstfga2i9hant5e'

const url = `${forteApiUrl}/auth/v1/oauth2/token`

const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret
})
})

const { data } = await res.json()

return {
accessToken: data.access_token,
expiresIn: data.expires_in,
tokenType: data.token_type
}
}

export interface CreateFortePaymentIntentArgs {
accessToken: string
tokenType: string
nftQuantity: string
recipientAddress: string
chainId: string
signature?: string
nftAddress: string
currencyAddress: string
targetContractAddress: string
nftName: string
imageUrl: string
tokenId: string
currencyQuantity: string
protocolConfig: ForteConfig
}

const forteCurrencyMap: { [chainId: string]: { [currencyAddress: string]: string } } = {
'1': {
[zeroAddress]: 'ETH',
['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'.toLowerCase()]: 'USDC_ETH'
},
'137': {
[zeroAddress]: 'POL',
['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'.toLowerCase()]: 'USDC_POLYGON'
},
'8453': {
[zeroAddress]: 'BASE_ETH'
}
}

const getForteCurrency = (chainId: string, currencyAddress: string) => {
return forteCurrencyMap[chainId]?.[currencyAddress.toLowerCase()] || 'ETH'
}

export const createFortePaymentIntent = async (forteApiUrl: string, args: CreateFortePaymentIntentArgs): Promise<any> => {
const {
accessToken,
tokenType,
recipientAddress,
chainId,
signature,
targetContractAddress,
nftName,
nftAddress,
nftQuantity,
imageUrl,
tokenId,
protocolConfig,
currencyAddress,
currencyQuantity
} = args

const network = findSupportedNetwork(chainId)

if (!network) {
throw new Error('Invalid chainId')
}

const url = `${forteApiUrl}/payments/v2/intent`
const forteBlockchainName = network.name.toLowerCase().replace('-', '_')
const idempotencyKey = `${recipientAddress}-${tokenId}-${targetContractAddress}-${nftName}-${new Date().getTime()}`

let body: { [key: string]: any } = {
blockchain: forteBlockchainName,
idempotency_key: idempotencyKey,
buyer: {
id: recipientAddress,
wallet: {
address: recipientAddress,
blockchain: forteBlockchainName
}
}
}

if (protocolConfig.protocol == 'mint') {
body = {
...body,
transaction_type: 'BUY_NFT_MINT',
currency: 'USD',
items: [
{
name: nftName,
quantity: nftQuantity,
price: {
amount: nftQuantity,
image_url: imageUrl,
title: nftName,
mint_data: {
nonce: `${targetContractAddress}-${Date.now()}`,
signature: signature,
token_ids: [tokenId],
protocol_address: targetContractAddress,
protocol: 'protocol-mint'
}
}
}
]
}
} else {
let listingData: { [key: string]: any } = {}

if (protocolConfig.protocol == 'seaport') {
listingData = {
protocol: protocolConfig.protocol,
order_hash: protocolConfig.orderHash,
protocol_address: protocolConfig.seaportProtocolAddress
}
} else if (protocolConfig.protocol == 'magiceden') {
listingData = {
protocol: protocolConfig.protocol,
auction_house: targetContractAddress,
token_address: nftAddress
}
}

body = {
...body,
transaction_type: 'BUY_NFT',
currency: getForteCurrency(chainId, currencyAddress),
items: [
{
amount: currencyQuantity,
id: '1',
image_url: imageUrl,
listing_data: listingData,
nft_data: {
contract_address: nftAddress,
token_id: tokenId
},
title: nftName
}
],
seller: {
wallet: {
address: protocolConfig.sellerAddress || '',
blockchain: forteBlockchainName
}
}
}
}

const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `${tokenType} ${accessToken}`
},
body: JSON.stringify(body)
})

if (!res.ok) {
throw new Error(`Failed to fetch widget data, with status: ${res.status}`)
}

const data = await res.json()

return data.data
}

export interface FetchFortePaymentStatusArgs {
accessToken: string
tokenType: string
paymentIntentId: string
}

export type FortePaymentStatus = 'Expired' | 'Created' | 'Declined' | 'Approved'

export interface FetchFortePaymentStatusReturn {
status: FortePaymentStatus
}

export const fetchFortePaymentStatus = async (
forteApiUrl: string,
args: FetchFortePaymentStatusArgs
): Promise<FetchFortePaymentStatusReturn> => {
const { accessToken, tokenType, paymentIntentId } = args

const url = `${forteApiUrl}/payments/v1/payments/statuses`

const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `${tokenType} ${accessToken}`
},
body: JSON.stringify({
payment_intent_ids: [paymentIntentId]
})
})

const { data } = await res.json()

return {
status: (data[0]?.status as FortePaymentStatus) || ''
}
}
Loading