@0xsequence/relayer 2.3.35 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +3862 -0
- package/LICENSE +0 -17
- package/README.md +1 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/preconditions/codec.d.ts +12 -0
- package/dist/preconditions/codec.d.ts.map +1 -0
- package/dist/preconditions/codec.js +125 -0
- package/dist/preconditions/index.d.ts +4 -0
- package/dist/preconditions/index.d.ts.map +1 -0
- package/dist/preconditions/index.js +3 -0
- package/dist/preconditions/selectors.d.ts +7 -0
- package/dist/preconditions/selectors.d.ts.map +1 -0
- package/dist/preconditions/selectors.js +27 -0
- package/dist/preconditions/types.d.ts +70 -0
- package/dist/preconditions/types.d.ts.map +1 -0
- package/dist/preconditions/types.js +203 -0
- package/dist/relayer/index.d.ts +45 -0
- package/dist/relayer/index.d.ts.map +1 -0
- package/dist/relayer/index.js +3 -0
- package/dist/relayer/relayer.d.ts +26 -0
- package/dist/relayer/relayer.d.ts.map +1 -0
- package/dist/relayer/relayer.js +7 -0
- package/dist/relayer/rpc-relayer/index.d.ts +38 -0
- package/dist/relayer/rpc-relayer/index.d.ts.map +1 -0
- package/dist/relayer/rpc-relayer/index.js +375 -0
- package/dist/{declarations/src → relayer}/rpc-relayer/relayer.gen.d.ts +218 -178
- package/dist/relayer/rpc-relayer/relayer.gen.d.ts.map +1 -0
- package/dist/relayer/rpc-relayer/relayer.gen.js +1246 -0
- package/dist/relayer/standard/abi.d.ts +73 -0
- package/dist/relayer/standard/abi.d.ts.map +1 -0
- package/dist/relayer/standard/abi.js +10 -0
- package/dist/relayer/standard/eip6963.d.ts +31 -0
- package/dist/relayer/standard/eip6963.d.ts.map +1 -0
- package/dist/relayer/standard/eip6963.js +51 -0
- package/dist/relayer/standard/index.d.ts +5 -0
- package/dist/relayer/standard/index.d.ts.map +1 -0
- package/dist/relayer/standard/index.js +4 -0
- package/dist/relayer/standard/local.d.ts +60 -0
- package/dist/relayer/standard/local.d.ts.map +1 -0
- package/dist/relayer/standard/local.js +285 -0
- package/dist/relayer/standard/pk-relayer.d.ts +28 -0
- package/dist/relayer/standard/pk-relayer.d.ts.map +1 -0
- package/dist/relayer/standard/pk-relayer.js +112 -0
- package/dist/relayer/standard/sequence.d.ts +27 -0
- package/dist/relayer/standard/sequence.d.ts.map +1 -0
- package/dist/relayer/standard/sequence.js +84 -0
- package/package.json +28 -25
- package/src/index.ts +3 -111
- package/src/preconditions/codec.ts +190 -0
- package/src/preconditions/index.ts +3 -0
- package/src/preconditions/selectors.ts +38 -0
- package/src/preconditions/types.ts +201 -0
- package/src/relayer/index.ts +60 -0
- package/src/relayer/relayer.ts +37 -0
- package/src/relayer/rpc-relayer/index.ts +449 -0
- package/src/relayer/rpc-relayer/relayer.gen.ts +2268 -0
- package/src/relayer/standard/abi.ts +13 -0
- package/src/relayer/standard/eip6963.ts +74 -0
- package/src/relayer/standard/index.ts +4 -0
- package/src/relayer/standard/local.ts +353 -0
- package/src/relayer/standard/pk-relayer.ts +138 -0
- package/src/relayer/standard/sequence.ts +110 -0
- package/test/preconditions/codec.test.ts +531 -0
- package/test/preconditions/preconditions.test.ts +283 -0
- package/test/preconditions/selectors.test.ts +415 -0
- package/test/preconditions/types.test.ts +443 -0
- package/test/relayer/relayer.test.ts +355 -0
- package/tsconfig.json +10 -0
- package/dist/0xsequence-relayer.cjs.d.ts +0 -2
- package/dist/0xsequence-relayer.cjs.dev.js +0 -1626
- package/dist/0xsequence-relayer.cjs.js +0 -7
- package/dist/0xsequence-relayer.cjs.prod.js +0 -1626
- package/dist/0xsequence-relayer.esm.js +0 -1613
- package/dist/declarations/src/index.d.ts +0 -42
- package/dist/declarations/src/local-relayer.d.ts +0 -35
- package/dist/declarations/src/provider-relayer.d.ts +0 -47
- package/dist/declarations/src/rpc-relayer/index.d.ts +0 -72
- package/src/local-relayer.ts +0 -125
- package/src/provider-relayer.ts +0 -284
- package/src/rpc-relayer/index.ts +0 -380
- package/src/rpc-relayer/relayer.gen.ts +0 -1900
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AbiFunction } from 'ox'
|
|
2
|
+
|
|
3
|
+
// ERC20 ABI functions
|
|
4
|
+
export const erc20BalanceOf = AbiFunction.from('function balanceOf(address) returns (uint256)')
|
|
5
|
+
export const erc20Allowance = AbiFunction.from('function allowance(address,address) returns (uint256)')
|
|
6
|
+
|
|
7
|
+
// ERC721 ABI functions
|
|
8
|
+
export const erc721OwnerOf = AbiFunction.from('function ownerOf(uint256) returns (address)')
|
|
9
|
+
export const erc721GetApproved = AbiFunction.from('function getApproved(uint256) returns (address)')
|
|
10
|
+
|
|
11
|
+
// ERC1155 ABI functions
|
|
12
|
+
export const erc1155BalanceOf = AbiFunction.from('function balanceOf(address,uint256) returns (uint256)')
|
|
13
|
+
export const erc1155IsApprovedForAll = AbiFunction.from('function isApprovedForAll(address,address) returns (bool)')
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createStore, EIP6963ProviderInfo, EIP6963ProviderDetail } from 'mipd'
|
|
2
|
+
import { EIP1193ProviderAdapter, LocalRelayer } from './local.js'
|
|
3
|
+
import { FeeOption, FeeQuote, OperationStatus, Relayer } from '../index.js'
|
|
4
|
+
import { Address, Hex } from 'ox'
|
|
5
|
+
import { Payload } from '@0xsequence/wallet-primitives'
|
|
6
|
+
import { FeeToken, TransactionPrecondition } from '../rpc-relayer/relayer.gen.js'
|
|
7
|
+
|
|
8
|
+
export class EIP6963Relayer implements Relayer {
|
|
9
|
+
public readonly kind: 'relayer' = 'relayer'
|
|
10
|
+
public readonly type = 'eip6963'
|
|
11
|
+
public readonly id: string
|
|
12
|
+
public readonly info: EIP6963ProviderInfo
|
|
13
|
+
private readonly relayer: LocalRelayer
|
|
14
|
+
|
|
15
|
+
constructor(detail: EIP6963ProviderDetail) {
|
|
16
|
+
this.info = detail.info
|
|
17
|
+
this.id = detail.info.uuid
|
|
18
|
+
|
|
19
|
+
this.relayer = new LocalRelayer(new EIP1193ProviderAdapter(detail.provider))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isAvailable(wallet: Address.Address, chainId: number): Promise<boolean> {
|
|
23
|
+
return this.relayer.isAvailable(wallet, chainId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
feeTokens(): Promise<{ isFeeRequired: boolean; tokens?: FeeToken[]; paymentAddress?: Address.Address }> {
|
|
27
|
+
return this.relayer.feeTokens()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
feeOptions(
|
|
31
|
+
wallet: Address.Address,
|
|
32
|
+
chainId: number,
|
|
33
|
+
calls: Payload.Call[],
|
|
34
|
+
): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
|
|
35
|
+
return this.relayer.feeOptions(wallet, chainId, calls)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async relay(to: Address.Address, data: Hex.Hex, chainId: number, _?: FeeQuote): Promise<{ opHash: Hex.Hex }> {
|
|
39
|
+
return this.relayer.relay(to, data, chainId)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
status(opHash: Hex.Hex, chainId: number): Promise<OperationStatus> {
|
|
43
|
+
return this.relayer.status(opHash, chainId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async checkPrecondition(precondition: TransactionPrecondition): Promise<boolean> {
|
|
47
|
+
return this.relayer.checkPrecondition(precondition)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Global store instance
|
|
52
|
+
let store: ReturnType<typeof createStore> | undefined
|
|
53
|
+
|
|
54
|
+
export function getEIP6963Store() {
|
|
55
|
+
if (!store) {
|
|
56
|
+
store = createStore()
|
|
57
|
+
}
|
|
58
|
+
return store
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let relayers: Map<string, EIP6963Relayer> = new Map()
|
|
62
|
+
|
|
63
|
+
export function getRelayers(): EIP6963Relayer[] {
|
|
64
|
+
const store = getEIP6963Store()
|
|
65
|
+
const providers = store.getProviders()
|
|
66
|
+
|
|
67
|
+
for (const detail of providers) {
|
|
68
|
+
if (!relayers.has(detail.info.uuid)) {
|
|
69
|
+
relayers.set(detail.info.uuid, new EIP6963Relayer(detail))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Array.from(relayers.values())
|
|
74
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { Constants, Payload } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { EIP1193Provider } from 'mipd'
|
|
3
|
+
import { AbiFunction, Address, Bytes, Hex, TransactionReceipt } from 'ox'
|
|
4
|
+
import { FeeOption, FeeQuote, OperationStatus, Relayer } from '../index.js'
|
|
5
|
+
import { FeeToken, TransactionPrecondition } from '../rpc-relayer/relayer.gen.js'
|
|
6
|
+
import { decodePrecondition } from '../../preconditions/index.js'
|
|
7
|
+
import {
|
|
8
|
+
erc20BalanceOf,
|
|
9
|
+
erc20Allowance,
|
|
10
|
+
erc721OwnerOf,
|
|
11
|
+
erc721GetApproved,
|
|
12
|
+
erc1155BalanceOf,
|
|
13
|
+
erc1155IsApprovedForAll,
|
|
14
|
+
} from './abi.js'
|
|
15
|
+
|
|
16
|
+
type GenericProviderTransactionReceipt = 'success' | 'failed' | 'unknown'
|
|
17
|
+
|
|
18
|
+
export interface GenericProvider {
|
|
19
|
+
sendTransaction(args: { to: Address.Address; data: Hex.Hex }, chainId: number): Promise<string | undefined>
|
|
20
|
+
getBalance(address: Address.Address): Promise<bigint>
|
|
21
|
+
call(args: { to: Address.Address; data: Hex.Hex }): Promise<string>
|
|
22
|
+
getTransactionReceipt(txHash: Hex.Hex, chainId: number): Promise<GenericProviderTransactionReceipt>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class LocalRelayer implements Relayer {
|
|
26
|
+
public readonly kind: 'relayer' = 'relayer'
|
|
27
|
+
public readonly type = 'local'
|
|
28
|
+
public readonly id = 'local'
|
|
29
|
+
|
|
30
|
+
constructor(public readonly provider: GenericProvider) {}
|
|
31
|
+
|
|
32
|
+
isAvailable(_wallet: Address.Address, _chainId: number): Promise<boolean> {
|
|
33
|
+
return Promise.resolve(true)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static createFromWindow(window: Window): LocalRelayer | undefined {
|
|
37
|
+
const eth = (window as any).ethereum
|
|
38
|
+
if (!eth) {
|
|
39
|
+
console.warn('Window.ethereum not found, skipping local relayer')
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new LocalRelayer(new EIP1193ProviderAdapter(eth))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static createFromProvider(provider: EIP1193Provider): LocalRelayer {
|
|
47
|
+
return new LocalRelayer(new EIP1193ProviderAdapter(provider))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
feeTokens(): Promise<{ isFeeRequired: boolean; tokens?: FeeToken[]; paymentAddress?: Address.Address }> {
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
isFeeRequired: false,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
feeOptions(
|
|
57
|
+
wallet: Address.Address,
|
|
58
|
+
chainId: number,
|
|
59
|
+
calls: Payload.Call[],
|
|
60
|
+
): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
|
|
61
|
+
return Promise.resolve({ options: [] })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private decodeCalls(data: Hex.Hex): Payload.Calls {
|
|
65
|
+
const executeSelector = AbiFunction.getSelector(Constants.EXECUTE)
|
|
66
|
+
|
|
67
|
+
let packedPayload
|
|
68
|
+
if (data.startsWith(executeSelector)) {
|
|
69
|
+
const decode = AbiFunction.decodeData(Constants.EXECUTE, data)
|
|
70
|
+
packedPayload = decode[0]
|
|
71
|
+
} else {
|
|
72
|
+
packedPayload = data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Payload.decode(Bytes.fromHex(packedPayload))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async relay(
|
|
79
|
+
to: Address.Address,
|
|
80
|
+
data: Hex.Hex,
|
|
81
|
+
chainId: number,
|
|
82
|
+
quote?: FeeQuote,
|
|
83
|
+
preconditions?: TransactionPrecondition[],
|
|
84
|
+
checkInterval: number = 5000,
|
|
85
|
+
): Promise<{ opHash: Hex.Hex }> {
|
|
86
|
+
// Helper function to check all preconditions
|
|
87
|
+
const checkAllPreconditions = async (): Promise<boolean> => {
|
|
88
|
+
if (!preconditions || preconditions.length === 0) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const precondition of preconditions) {
|
|
93
|
+
const isValid = await this.checkPrecondition(precondition)
|
|
94
|
+
if (!isValid) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check preconditions immediately
|
|
102
|
+
if (await checkAllPreconditions()) {
|
|
103
|
+
// If all preconditions are met, relay the transaction
|
|
104
|
+
const txHash = await this.provider.sendTransaction(
|
|
105
|
+
{
|
|
106
|
+
to,
|
|
107
|
+
data,
|
|
108
|
+
},
|
|
109
|
+
chainId,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// TODO: Return the opHash instead, but solve the `status` function
|
|
113
|
+
// to properly fetch the receipt from an opHash instead of a txHash
|
|
114
|
+
return { opHash: txHash as Hex.Hex }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If not all preconditions are met, set up event listeners and polling
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
let timeoutId: NodeJS.Timeout
|
|
120
|
+
let isResolved = false
|
|
121
|
+
|
|
122
|
+
// Function to check and relay
|
|
123
|
+
const checkAndRelay = async () => {
|
|
124
|
+
try {
|
|
125
|
+
if (isResolved) return
|
|
126
|
+
|
|
127
|
+
if (await checkAllPreconditions()) {
|
|
128
|
+
isResolved = true
|
|
129
|
+
clearTimeout(timeoutId)
|
|
130
|
+
const txHash = await this.provider.sendTransaction(
|
|
131
|
+
{
|
|
132
|
+
to,
|
|
133
|
+
data,
|
|
134
|
+
},
|
|
135
|
+
chainId,
|
|
136
|
+
)
|
|
137
|
+
resolve({ opHash: txHash as Hex.Hex })
|
|
138
|
+
} else {
|
|
139
|
+
// Schedule next check
|
|
140
|
+
timeoutId = setTimeout(checkAndRelay, checkInterval)
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
isResolved = true
|
|
144
|
+
clearTimeout(timeoutId)
|
|
145
|
+
reject(error)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Start checking
|
|
150
|
+
timeoutId = setTimeout(checkAndRelay, checkInterval)
|
|
151
|
+
|
|
152
|
+
// Cleanup function
|
|
153
|
+
return () => {
|
|
154
|
+
isResolved = true
|
|
155
|
+
clearTimeout(timeoutId)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async status(opHash: Hex.Hex, chainId: number): Promise<OperationStatus> {
|
|
161
|
+
const receipt = await this.provider.getTransactionReceipt(opHash, chainId)
|
|
162
|
+
if (receipt === 'unknown') {
|
|
163
|
+
// Could be pending but we don't know
|
|
164
|
+
return { status: 'unknown' }
|
|
165
|
+
}
|
|
166
|
+
return receipt === 'success'
|
|
167
|
+
? { status: 'confirmed', transactionHash: opHash }
|
|
168
|
+
: { status: 'failed', reason: 'failed' }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async checkPrecondition(precondition: TransactionPrecondition): Promise<boolean> {
|
|
172
|
+
const decoded = decodePrecondition(precondition)
|
|
173
|
+
|
|
174
|
+
if (!decoded) {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
switch (decoded.type()) {
|
|
179
|
+
case 'native-balance': {
|
|
180
|
+
const native = decoded as any
|
|
181
|
+
const balance = await this.provider.getBalance(native.address.toString())
|
|
182
|
+
if (native.min !== undefined && balance < native.min) {
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
if (native.max !== undefined && balance > native.max) {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'erc20-balance': {
|
|
192
|
+
const erc20 = decoded as any
|
|
193
|
+
const data = AbiFunction.encodeData(erc20BalanceOf, [erc20.address.toString()])
|
|
194
|
+
const result = await this.provider.call({
|
|
195
|
+
to: erc20.token.toString(),
|
|
196
|
+
data,
|
|
197
|
+
})
|
|
198
|
+
const balance = BigInt(result)
|
|
199
|
+
if (erc20.min !== undefined && balance < erc20.min) {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
if (erc20.max !== undefined && balance > erc20.max) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'erc20-approval': {
|
|
209
|
+
const erc20 = decoded as any
|
|
210
|
+
const data = AbiFunction.encodeData(erc20Allowance, [erc20.address.toString(), erc20.operator.toString()])
|
|
211
|
+
const result = await this.provider.call({
|
|
212
|
+
to: erc20.token.toString(),
|
|
213
|
+
data,
|
|
214
|
+
})
|
|
215
|
+
const allowance = BigInt(result)
|
|
216
|
+
return allowance >= erc20.min
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'erc721-ownership': {
|
|
220
|
+
const erc721 = decoded as any
|
|
221
|
+
const data = AbiFunction.encodeData(erc721OwnerOf, [erc721.tokenId])
|
|
222
|
+
const result = await this.provider.call({
|
|
223
|
+
to: erc721.token.toString(),
|
|
224
|
+
data,
|
|
225
|
+
})
|
|
226
|
+
const owner = '0x' + result.slice(26)
|
|
227
|
+
const isOwner = owner.toLowerCase() === erc721.address.toString().toLowerCase()
|
|
228
|
+
return erc721.owned === undefined ? isOwner : erc721.owned === isOwner
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case 'erc721-approval': {
|
|
232
|
+
const erc721 = decoded as any
|
|
233
|
+
const data = AbiFunction.encodeData(erc721GetApproved, [erc721.tokenId])
|
|
234
|
+
const result = await this.provider.call({
|
|
235
|
+
to: erc721.token.toString(),
|
|
236
|
+
data,
|
|
237
|
+
})
|
|
238
|
+
const approved = '0x' + result.slice(26)
|
|
239
|
+
return approved.toLowerCase() === erc721.operator.toString().toLowerCase()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case 'erc1155-balance': {
|
|
243
|
+
const erc1155 = decoded as any
|
|
244
|
+
const data = AbiFunction.encodeData(erc1155BalanceOf, [erc1155.address.toString(), erc1155.tokenId])
|
|
245
|
+
const result = await this.provider.call({
|
|
246
|
+
to: erc1155.token.toString(),
|
|
247
|
+
data,
|
|
248
|
+
})
|
|
249
|
+
const balance = BigInt(result)
|
|
250
|
+
if (erc1155.min !== undefined && balance < erc1155.min) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
if (erc1155.max !== undefined && balance > erc1155.max) {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case 'erc1155-approval': {
|
|
260
|
+
const erc1155 = decoded as any
|
|
261
|
+
const data = AbiFunction.encodeData(erc1155IsApprovedForAll, [
|
|
262
|
+
erc1155.address.toString(),
|
|
263
|
+
erc1155.operator.toString(),
|
|
264
|
+
])
|
|
265
|
+
const result = await this.provider.call({
|
|
266
|
+
to: erc1155.token.toString(),
|
|
267
|
+
data,
|
|
268
|
+
})
|
|
269
|
+
return BigInt(result) === 1n
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
default:
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export class EIP1193ProviderAdapter implements GenericProvider {
|
|
279
|
+
constructor(private readonly provider: EIP1193Provider) {}
|
|
280
|
+
|
|
281
|
+
private async trySwitchChain(chainId: number) {
|
|
282
|
+
try {
|
|
283
|
+
await this.provider.request({
|
|
284
|
+
method: 'wallet_switchEthereumChain',
|
|
285
|
+
params: [
|
|
286
|
+
{
|
|
287
|
+
chainId: `0x${chainId.toString(16)}`,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
})
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// Log and continue
|
|
293
|
+
console.error('Error switching chain', error)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async sendTransaction(args: { to: Address.Address; data: Hex.Hex }, chainId: number) {
|
|
298
|
+
const accounts: Address.Address[] = await this.provider.request({ method: 'eth_requestAccounts' })
|
|
299
|
+
const from = accounts[0]
|
|
300
|
+
|
|
301
|
+
if (!from) {
|
|
302
|
+
console.warn('No account selected, skipping local relayer')
|
|
303
|
+
return undefined
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await this.trySwitchChain(chainId)
|
|
307
|
+
|
|
308
|
+
const tx = await this.provider.request({
|
|
309
|
+
method: 'eth_sendTransaction',
|
|
310
|
+
params: [
|
|
311
|
+
{
|
|
312
|
+
from,
|
|
313
|
+
to: args.to,
|
|
314
|
+
data: args.data,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
return tx
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async getBalance(address: Address.Address) {
|
|
323
|
+
const balance = await this.provider.request({
|
|
324
|
+
method: 'eth_getBalance',
|
|
325
|
+
params: [address, 'latest'],
|
|
326
|
+
})
|
|
327
|
+
return BigInt(balance)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async call(args: { to: Address.Address; data: Hex.Hex }) {
|
|
331
|
+
return await this.provider.request({
|
|
332
|
+
method: 'eth_call',
|
|
333
|
+
params: [args, 'latest'],
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async getTransactionReceipt(txHash: Hex.Hex, chainId: number) {
|
|
338
|
+
await this.trySwitchChain(chainId)
|
|
339
|
+
|
|
340
|
+
const rpcReceipt = await this.provider.request({ method: 'eth_getTransactionReceipt', params: [txHash] })
|
|
341
|
+
|
|
342
|
+
if (rpcReceipt) {
|
|
343
|
+
const receipt = TransactionReceipt.fromRpc(rpcReceipt as any)
|
|
344
|
+
if (receipt?.status === 'success') {
|
|
345
|
+
return 'success'
|
|
346
|
+
} else if (receipt?.status === 'reverted') {
|
|
347
|
+
return 'failed'
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return 'unknown'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Payload, Precondition } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { Address, Hex, Provider, Secp256k1, TransactionEnvelopeEip1559, TransactionReceipt } from 'ox'
|
|
3
|
+
import { LocalRelayer } from './local.js'
|
|
4
|
+
import { FeeOption, FeeQuote, OperationStatus, Relayer } from '../index.js'
|
|
5
|
+
import { FeeToken } from '../rpc-relayer/relayer.gen.js'
|
|
6
|
+
|
|
7
|
+
export class PkRelayer implements Relayer {
|
|
8
|
+
public readonly kind: 'relayer' = 'relayer'
|
|
9
|
+
public readonly type = 'pk'
|
|
10
|
+
public readonly id = 'pk'
|
|
11
|
+
private readonly relayer: LocalRelayer
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
privateKey: Hex.Hex,
|
|
15
|
+
private readonly provider: Provider.Provider,
|
|
16
|
+
) {
|
|
17
|
+
const relayerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey }))
|
|
18
|
+
this.relayer = new LocalRelayer({
|
|
19
|
+
sendTransaction: async (args, chainId) => {
|
|
20
|
+
const providerChainId = Number(await this.provider.request({ method: 'eth_chainId' }))
|
|
21
|
+
if (providerChainId !== chainId) {
|
|
22
|
+
throw new Error('Provider chain id does not match relayer chain id')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const oxArgs = { ...args, to: args.to as `0x${string}`, data: args.data as `0x${string}` }
|
|
26
|
+
// Estimate gas with a safety buffer
|
|
27
|
+
const estimatedGas = BigInt(await this.provider.request({ method: 'eth_estimateGas', params: [oxArgs] }))
|
|
28
|
+
const safeGasLimit = estimatedGas > 21000n ? (estimatedGas * 12n) / 10n : 50000n
|
|
29
|
+
|
|
30
|
+
// Get base fee and priority fee
|
|
31
|
+
const baseFee = BigInt(await this.provider.request({ method: 'eth_gasPrice' }))
|
|
32
|
+
const priorityFee = 100000000n // 0.1 gwei priority fee
|
|
33
|
+
const maxFeePerGas = baseFee + priorityFee
|
|
34
|
+
|
|
35
|
+
// Check sender have enough balance
|
|
36
|
+
const senderBalance = BigInt(
|
|
37
|
+
await this.provider.request({ method: 'eth_getBalance', params: [relayerAddress, 'latest'] }),
|
|
38
|
+
)
|
|
39
|
+
if (senderBalance < maxFeePerGas * safeGasLimit) {
|
|
40
|
+
console.log('Sender balance:', senderBalance.toString(), 'wei')
|
|
41
|
+
throw new Error('Sender has insufficient balance to pay for gas')
|
|
42
|
+
}
|
|
43
|
+
const nonce = BigInt(
|
|
44
|
+
await this.provider.request({
|
|
45
|
+
method: 'eth_getTransactionCount',
|
|
46
|
+
params: [relayerAddress, 'latest'],
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Build the relay envelope
|
|
51
|
+
const relayEnvelope = TransactionEnvelopeEip1559.from({
|
|
52
|
+
chainId: Number(chainId),
|
|
53
|
+
type: 'eip1559',
|
|
54
|
+
from: relayerAddress,
|
|
55
|
+
to: oxArgs.to,
|
|
56
|
+
data: oxArgs.data,
|
|
57
|
+
gas: safeGasLimit,
|
|
58
|
+
maxFeePerGas: maxFeePerGas,
|
|
59
|
+
maxPriorityFeePerGas: priorityFee,
|
|
60
|
+
nonce: nonce,
|
|
61
|
+
value: 0n,
|
|
62
|
+
})
|
|
63
|
+
const relayerSignature = Secp256k1.sign({
|
|
64
|
+
payload: TransactionEnvelopeEip1559.getSignPayload(relayEnvelope),
|
|
65
|
+
privateKey: privateKey,
|
|
66
|
+
})
|
|
67
|
+
const signedRelayEnvelope = TransactionEnvelopeEip1559.from(relayEnvelope, {
|
|
68
|
+
signature: relayerSignature,
|
|
69
|
+
})
|
|
70
|
+
const tx = await this.provider.request({
|
|
71
|
+
method: 'eth_sendRawTransaction',
|
|
72
|
+
params: [TransactionEnvelopeEip1559.serialize(signedRelayEnvelope)],
|
|
73
|
+
})
|
|
74
|
+
return tx
|
|
75
|
+
},
|
|
76
|
+
getBalance: async (address: string): Promise<bigint> => {
|
|
77
|
+
const balanceHex = await this.provider.request({
|
|
78
|
+
method: 'eth_getBalance',
|
|
79
|
+
params: [address as Address.Address, 'latest'],
|
|
80
|
+
})
|
|
81
|
+
return BigInt(balanceHex)
|
|
82
|
+
},
|
|
83
|
+
call: async (args: { to: string; data: string }): Promise<string> => {
|
|
84
|
+
const callArgs = { to: args.to as `0x${string}`, data: args.data as `0x${string}` }
|
|
85
|
+
return await this.provider.request({ method: 'eth_call', params: [callArgs, 'latest'] })
|
|
86
|
+
},
|
|
87
|
+
getTransactionReceipt: async (txHash: string, chainId: number) => {
|
|
88
|
+
Hex.assert(txHash)
|
|
89
|
+
|
|
90
|
+
const providerChainId = Number(await this.provider.request({ method: 'eth_chainId' }))
|
|
91
|
+
if (providerChainId !== chainId) {
|
|
92
|
+
throw new Error('Provider chain id does not match relayer chain id')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rpcReceipt = await this.provider.request({ method: 'eth_getTransactionReceipt', params: [txHash] })
|
|
96
|
+
if (!rpcReceipt) {
|
|
97
|
+
return 'unknown'
|
|
98
|
+
}
|
|
99
|
+
const receipt = TransactionReceipt.fromRpc(rpcReceipt)
|
|
100
|
+
return receipt.status === 'success' ? 'success' : 'failed'
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async isAvailable(_wallet: Address.Address, chainId: number): Promise<boolean> {
|
|
106
|
+
const providerChainId = Number(await this.provider.request({ method: 'eth_chainId' }))
|
|
107
|
+
return providerChainId === chainId
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
feeTokens(): Promise<{ isFeeRequired: boolean; tokens?: FeeToken[]; paymentAddress?: Address.Address }> {
|
|
111
|
+
return this.relayer.feeTokens()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
feeOptions(
|
|
115
|
+
wallet: Address.Address,
|
|
116
|
+
chainId: number,
|
|
117
|
+
calls: Payload.Call[],
|
|
118
|
+
): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
|
|
119
|
+
return this.relayer.feeOptions(wallet, chainId, calls)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async relay(to: Address.Address, data: Hex.Hex, chainId: number, _?: FeeQuote): Promise<{ opHash: Hex.Hex }> {
|
|
123
|
+
const providerChainId = Number(await this.provider.request({ method: 'eth_chainId' }))
|
|
124
|
+
if (providerChainId !== chainId) {
|
|
125
|
+
throw new Error('Provider chain id does not match relayer chain id')
|
|
126
|
+
}
|
|
127
|
+
return this.relayer.relay(to, data, chainId)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
status(opHash: Hex.Hex, chainId: number): Promise<OperationStatus> {
|
|
131
|
+
return this.relayer.status(opHash, chainId)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async checkPrecondition(precondition: Precondition.Precondition): Promise<boolean> {
|
|
135
|
+
// TODO: Implement precondition check
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ETHTxnStatus, TransactionPrecondition, Relayer as Service, FeeToken } from '../rpc-relayer/relayer.gen.js'
|
|
2
|
+
import { Payload } from '@0xsequence/wallet-primitives'
|
|
3
|
+
import { AbiFunction, Address, Bytes, Hex } from 'ox'
|
|
4
|
+
import { FeeOption, FeeQuote, OperationStatus, Relayer } from '../index.js'
|
|
5
|
+
export class SequenceRelayer implements Relayer {
|
|
6
|
+
public readonly kind: 'relayer' = 'relayer'
|
|
7
|
+
public readonly type = 'sequence'
|
|
8
|
+
readonly id = 'sequence'
|
|
9
|
+
|
|
10
|
+
private readonly service: Service
|
|
11
|
+
|
|
12
|
+
constructor(host: string) {
|
|
13
|
+
this.service = new Service(host, fetch)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async isAvailable(_wallet: Address.Address, _chainId: number): Promise<boolean> {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async feeTokens(): Promise<{ isFeeRequired: boolean; tokens?: FeeToken[]; paymentAddress?: Address.Address }> {
|
|
21
|
+
const { isFeeRequired, tokens, paymentAddress } = await this.service.feeTokens()
|
|
22
|
+
if (isFeeRequired) {
|
|
23
|
+
Address.assert(paymentAddress)
|
|
24
|
+
return {
|
|
25
|
+
isFeeRequired,
|
|
26
|
+
tokens,
|
|
27
|
+
paymentAddress,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Not required
|
|
31
|
+
return {
|
|
32
|
+
isFeeRequired,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async feeOptions(
|
|
37
|
+
wallet: Address.Address,
|
|
38
|
+
_chainId: number,
|
|
39
|
+
calls: Payload.Call[],
|
|
40
|
+
): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
|
|
41
|
+
const to = wallet // TODO: this might be the guest module
|
|
42
|
+
const execute = AbiFunction.from('function execute(bytes calldata _payload, bytes calldata _signature)')
|
|
43
|
+
const payload = Payload.encode({ type: 'call', space: 0n, nonce: 0n, calls }, to)
|
|
44
|
+
const signature = '0x0001' // TODO: use a stub signature
|
|
45
|
+
const data = AbiFunction.encodeData(execute, [Bytes.toHex(payload), signature])
|
|
46
|
+
|
|
47
|
+
const { options, quote } = await this.service.feeOptions({ wallet, to, data })
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
options,
|
|
51
|
+
quote: quote ? { _tag: 'FeeQuote', _quote: quote } : undefined,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async checkPrecondition(precondition: TransactionPrecondition): Promise<boolean> {
|
|
56
|
+
// TODO: implement
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async relay(to: Address.Address, data: Hex.Hex, _chainId: number, quote?: FeeQuote): Promise<{ opHash: Hex.Hex }> {
|
|
61
|
+
const walletAddress = to // TODO: pass wallet address or stop requiring it
|
|
62
|
+
|
|
63
|
+
const { txnHash } = await this.service.sendMetaTxn({
|
|
64
|
+
call: { walletAddress, contract: to, input: data },
|
|
65
|
+
quote: quote && (quote._quote as string),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return { opHash: `0x${txnHash}` }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async status(opHash: Hex.Hex, _chainId: number): Promise<OperationStatus> {
|
|
72
|
+
try {
|
|
73
|
+
const {
|
|
74
|
+
receipt: { status, revertReason, txnReceipt },
|
|
75
|
+
} = await this.service.getMetaTxnReceipt({ metaTxID: opHash })
|
|
76
|
+
|
|
77
|
+
switch (status) {
|
|
78
|
+
case ETHTxnStatus.UNKNOWN:
|
|
79
|
+
return { status: 'unknown' }
|
|
80
|
+
|
|
81
|
+
case ETHTxnStatus.DROPPED:
|
|
82
|
+
return { status: 'failed', reason: revertReason ?? status }
|
|
83
|
+
|
|
84
|
+
case ETHTxnStatus.QUEUED:
|
|
85
|
+
return { status: 'pending' }
|
|
86
|
+
|
|
87
|
+
case ETHTxnStatus.SENT:
|
|
88
|
+
return { status: 'pending' }
|
|
89
|
+
|
|
90
|
+
case ETHTxnStatus.SUCCEEDED: {
|
|
91
|
+
const receipt = JSON.parse(txnReceipt)
|
|
92
|
+
const transactionHash = receipt.transactionHash
|
|
93
|
+
Hex.assert(transactionHash)
|
|
94
|
+
return { status: 'confirmed', transactionHash }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case ETHTxnStatus.PARTIALLY_FAILED:
|
|
98
|
+
return { status: 'failed', reason: revertReason ?? status }
|
|
99
|
+
|
|
100
|
+
case ETHTxnStatus.FAILED:
|
|
101
|
+
return { status: 'failed', reason: revertReason ?? status }
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`unknown transaction status '${status}'`)
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
return { status: 'pending' }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|