Skip to content

Commit e568390

Browse files
committed
✨ feat: finish gasless examples
1 parent c616ca1 commit e568390

12 files changed

+207
-20
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "api-example",
3-
"private": true,
4-
"version": "0.0.0",
53
"type": "module",
4+
"version": "0.0.0",
5+
"private": true,
66
"scripts": {
77
"dev": "vite",
88
"build": "tsc && vite build",

src/App.tsx

+22-9
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { BrowserProvider } from 'ethers'
33
import type { ChangeEvent } from 'react'
44
import { useState } from 'react'
55

6+
import './patch'
7+
68
import './App.css'
79

8-
import { checkNeedAllowance, getAllowances, getRoute, permit, setAllowance } from './flow'
9-
import { isValidEthereumAddress } from './helpers'
10+
import { checkNeedAllowance, gaslessTransfer, getAllowances, getRoute, permit, setAllowance } from './flow'
11+
import { gaslessExecute } from './flow/gaslessExecute'
12+
import { checkTransferRoute, isValidEthereumAddress } from './helpers'
13+
import { STEPS } from './constants'
1014

1115
function App() {
1216
const [provider, setProvider] = useState<BrowserProvider | null>(null)
@@ -47,15 +51,17 @@ function App() {
4751
setStep(3)
4852
const isNeedAllowance = await checkNeedAllowance(route, allowances)
4953

54+
setStep(4)
5055
await signer.provider.send('wallet_switchEthereumChain', [
5156
{
5257
chainId: `0x${route.fromToken.chainId.toString(16)}`
5358
}
5459
])
5560

56-
let allowanceResult: Signature | null = null
61+
let allowanceResult: Signature | undefined
5762

5863
if (isNeedAllowance) {
64+
setStep(5)
5965
try {
6066
allowanceResult = await permit(route, allowances, signer)
6167
} catch (e) {
@@ -65,8 +71,18 @@ function App() {
6571
}
6672
}
6773

68-
// eslint-disable-next-line no-debugger, no-restricted-syntax
69-
debugger
74+
setStep(6)
75+
const isTransferRoute = checkTransferRoute(route)
76+
77+
let resultHash: string
78+
if (isTransferRoute) {
79+
resultHash = await gaslessTransfer(route, signer, allowances, allowanceResult)
80+
} else {
81+
resultHash = await gaslessExecute(route, signer, allowances, allowanceResult)
82+
}
83+
84+
// eslint-disable-next-line no-console
85+
console.log('Transaction hash:', resultHash)
7086

7187
// Finish!
7288
setStep(0)
@@ -112,13 +128,10 @@ function App() {
112128
Send
113129
</button>
114130

115-
{/* Progress */}
116131
{step > 0 && (
117132
<div className="progress">
118133
<p>
119-
{step === 1 && 'Get allowances from backend...'}
120-
{step === 2 && 'Fetch route...'}
121-
{step === 3 && 'Allowance check...'}
134+
{STEPS[step]}
122135
</p>
123136
</div>
124137
)}

src/constants.ts

+34-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import type { TPermitToken, TPermitTokens } from './types/permit'
22

3-
const API_URL = 'https://api.staging.leto.xyz/gia/'
3+
const API_URL = 'https://gia.leto.xyz/'
4+
5+
const STEPS = [
6+
'Initial',
7+
'Get allowances from backend...',
8+
'Fetch route...',
9+
'Allowance check...',
10+
'Prepare transaction...',
11+
'Set allowance...',
12+
'Sign transaction...'
13+
]
414

515
const ERC2612_TOKENS: TPermitToken[] = [
616
{
@@ -71,4 +81,26 @@ const PermitMessage = [
7181
{ name: 'deadline', type: 'uint256' }
7282
]
7383

74-
export { API_URL, SUPPORTED_TOKENS, MAX_UINT256, EIP712PermitDomains, DaiPermitMessage, PermitMessage }
84+
const TypesTransfer = {
85+
TransferCall: [
86+
{ name: 'token', type: 'address' },
87+
{ name: 'to', type: 'address' },
88+
{ name: 'amount', type: 'uint256' },
89+
{ name: 'fee', type: 'uint256' },
90+
{ name: 'nonce', type: 'uint256' }
91+
]
92+
}
93+
94+
const TypesExecute = {
95+
ExecutionCall: [
96+
{ name: 'token', type: 'address' },
97+
{ name: 'amount', type: 'uint256' },
98+
{ name: 'quoter', type: 'address' },
99+
{ name: 'quoteData', type: 'bytes' },
100+
{ name: 'fee', type: 'uint256' },
101+
{ name: 'executionData', type: 'bytes' },
102+
{ name: 'value', type: 'uint256' }
103+
]
104+
}
105+
106+
export { API_URL, STEPS, SUPPORTED_TOKENS, MAX_UINT256, EIP712PermitDomains, DaiPermitMessage, PermitMessage, TypesTransfer, TypesExecute }

src/flow/checkNeedAllowance.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import { checkTransferRoute } from '../helpers'
12
import type { TAllowance, TGiaContractName } from '../types/allowance'
23
import type { TGiaRoute } from '../types/route'
34

45
function getAllowance(route: TGiaRoute, allowances: TAllowance[]) {
5-
const fromAddress = route.fromToken.address
6-
const toAddress = route.toToken.address
6+
const { address, chainId } = route.fromToken
77

8-
const contractName: TGiaContractName = fromAddress === toAddress ? 'ViaGaslessRelay' : 'ViaRouter'
8+
const contractName: TGiaContractName = checkTransferRoute(route) ? 'ViaGaslessRelay' : 'ViaRouter'
99

1010
return allowances.find(
11-
allowance => allowance.contractName === contractName && allowance.tokenAddress === fromAddress && allowance.chainId === route.fromToken.chainId
11+
a => a.contractName === contractName && a.tokenAddress === address && a.chainId === chainId
1212
)
1313
}
1414

src/flow/gasless.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { TypedDataDomain } from 'ethers'
2+
import { zeroPadValue } from 'ethers'
3+
import type { TAllowance } from '../types/allowance'
4+
import type { TGiaRoute } from '../types/route'
5+
6+
function getGaslessContractAddress(chainId: number, allowances: TAllowance[]) {
7+
return allowances.find(allowance => allowance.chainId === chainId && allowance.contractName === 'ViaGaslessRelay')
8+
?.contractAddress as `0x${string}`
9+
}
10+
11+
function getGaslessRelay(route: TGiaRoute, allowances: TAllowance[]): TypedDataDomain {
12+
const { chainId } = route.fromToken
13+
14+
const verifyingContract = getGaslessContractAddress(chainId, allowances)
15+
return {
16+
name: 'Transfer Money',
17+
version: '1.0.0',
18+
chainId: zeroPadValue(`0x${chainId.toString(16).padStart(32, '0')}`, 32),
19+
verifyingContract
20+
}
21+
}
22+
23+
export { getGaslessRelay }

src/flow/gaslessExecute.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { JsonRpcSigner, Signature } from 'ethers'
2+
import { API_URL, TypesExecute } from '../constants'
3+
import type { TAllowance } from '../types/allowance'
4+
import type { TGiaRoute } from '../types/route'
5+
import { getGaslessRelay } from './gasless'
6+
7+
async function gaslessExecute(route: TGiaRoute, signer: JsonRpcSigner, allowances: TAllowance[], permit?: Signature) {
8+
const { recipientAddress, senderAddress, fromToken, gasless } = route
9+
const { amount, address } = fromToken
10+
11+
const { quoteCallData, executeCallData, executeValue, quoterAddress } = gasless.executeExtra
12+
13+
const gaslessRelay = getGaslessRelay(route, allowances)
14+
15+
const fee = route.gasless.fee.withBonus || '0' // this is a string
16+
17+
const signConfig = {
18+
token: address,
19+
amount,
20+
quoter: quoterAddress,
21+
quoteData: quoteCallData === '0x' ? quoteCallData : `0x${quoteCallData}`,
22+
fee,
23+
executionData: `0x${executeCallData}`,
24+
value: executeValue || '0'
25+
}
26+
27+
const signedMessage = await signer.signTypedData(gaslessRelay, TypesExecute, signConfig)
28+
const urlParams = new URLSearchParams({
29+
routeUUID: route.uuid,
30+
signedMessage,
31+
recipientAddress,
32+
senderAddress
33+
})
34+
35+
if (permit) {
36+
urlParams.append('permitR', permit.r.slice(2))
37+
urlParams.append('permitS', permit.s.slice(2))
38+
urlParams.append('permitV', permit.v.toString())
39+
}
40+
41+
const url = `${API_URL}v1/leto/gasless-execute?${urlParams.toString()}`
42+
43+
const res = await fetch(url, {
44+
method: 'GET'
45+
})
46+
47+
return res.json()
48+
}
49+
50+
export { gaslessExecute }

src/flow/gaslessTransfer.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { JsonRpcSigner, Signature } from 'ethers'
2+
import { randomBytes } from 'ethers'
3+
import { API_URL, TypesTransfer } from '../constants'
4+
import type { TAllowance } from '../types/allowance'
5+
import type { TGiaRoute } from '../types/route'
6+
import { getGaslessRelay } from './gasless'
7+
8+
async function gaslessTransfer(route: TGiaRoute, signer: JsonRpcSigner, allowances: TAllowance[], permit?: Signature) {
9+
const { recipientAddress, senderAddress } = route
10+
11+
const gaslessRelay = getGaslessRelay(route, allowances)
12+
13+
const nonce = new DataView(randomBytes(32).buffer).getBigUint64(0, true)
14+
const gaslessFeeAmount = route.gasless.fee.withBonus || '0' // this is a string
15+
16+
const config = {
17+
token: route.fromToken.address,
18+
to: route.recipientAddress,
19+
amount: route.fromToken.amount.toString(),
20+
fee: gaslessFeeAmount,
21+
nonce: nonce.toString()
22+
}
23+
24+
const signedMessage = await signer.signTypedData(
25+
gaslessRelay,
26+
TypesTransfer,
27+
config
28+
)
29+
30+
const params: any = {
31+
nonce: nonce.toString(),
32+
routeUUID: route.uuid,
33+
signedMessage,
34+
recipientAddress,
35+
senderAddress
36+
}
37+
38+
if (permit) {
39+
params.permitR = permit.r.slice(2)
40+
params.permitS = permit.s.slice(2)
41+
params.permitV = permit.v.toString()
42+
}
43+
44+
const url = `${API_URL}v1/leto/gasless-transfer`
45+
46+
const res = await fetch(url, {
47+
method: 'POST',
48+
body: JSON.stringify(params)
49+
})
50+
51+
return res.json()
52+
}
53+
54+
export { gaslessTransfer }

src/flow/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './checkNeedAllowance'
2+
export * from './gaslessTransfer'
23
export * from './getAllowances'
34
export * from './getRoute'
45
export * from './permit'

src/flow/setAllowance.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JsonRpcSigner } from 'ethers'
22
import type { TGiaRoute } from '../types/route'
33

44
async function setAllowance(route: TGiaRoute, signer: JsonRpcSigner) {
5-
5+
const transaction = await signer.sendTransaction(route.approveTx)
66
}
77

88
export { setAllowance }

src/helpers.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
import type { TGiaRoute } from './types/route'
2+
13
function isValidEthereumAddress(address: string) {
24
return /^0x[a-fA-F0-9]{40}$/.test(address)
35
}
46

5-
export { isValidEthereumAddress }
7+
function checkTransferRoute(route: TGiaRoute) {
8+
const { fromToken, toToken } = route
9+
10+
return fromToken.address === toToken.address && fromToken.chainId === toToken.chainId
11+
}
12+
13+
export { isValidEthereumAddress, checkTransferRoute }

src/patch.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable no-extend-native */
2+
3+
// @ts-expect-error 🚧 ETHERS 6.1.0 IS BROKEN. THIS IS A WORKAROUND
4+
BigInt.prototype.toJSON = function () {
5+
return this.toString()
6+
}

src/types/route.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type TToken = {
2727
}
2828

2929
type TGiaRoute = {
30-
approveTx: TSimpleTxData | null
30+
approveTx: TSimpleTxData
3131
estimateTimeSeconds: number
3232
fromToken: TToken
3333
gasless: {

0 commit comments

Comments
 (0)