- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
302 lines
9.7 KiB
TypeScript
302 lines
9.7 KiB
TypeScript
import * as Address from 'ox/Address'
|
|
import * as Hash from 'ox/Hash'
|
|
import * as Hex from 'ox/Hex'
|
|
import * as Provider from 'ox/Provider'
|
|
import * as RpcRequest from 'ox/RpcRequest'
|
|
import type { LocalAccount } from '../accounts/types.js'
|
|
import { getTransactionReceipt } from '../actions/public/getTransactionReceipt.js'
|
|
import { sendTransaction } from '../actions/wallet/sendTransaction.js'
|
|
import { sendTransactionSync } from '../actions/wallet/sendTransactionSync.js'
|
|
import { createClient } from '../clients/createClient.js'
|
|
import {
|
|
createTransport,
|
|
type Transport,
|
|
} from '../clients/transports/createTransport.js'
|
|
import type { Chain } from '../types/chain.js'
|
|
import type { ChainConfig } from './chainConfig.js'
|
|
import * as Transaction from './Transaction.js'
|
|
|
|
type RelayProxyParameters = {
|
|
/** Policy for how the relay should handle sponsored transactions. Defaults to `'sign-only'`. */
|
|
policy?: 'sign-only' | 'sign-and-broadcast' | undefined
|
|
}
|
|
|
|
export type FeePayer = Transport<typeof withFeePayer.type>
|
|
export type Relay = Transport<typeof withRelay.type>
|
|
|
|
/**
|
|
* Creates a relay transport that routes requests between
|
|
* the default transport or the relay transport.
|
|
*
|
|
* All `eth_fillTransaction` requests are sent to the relay with the request's
|
|
* `feePayer` value preserved so the relay can decide whether to sponsor the transaction.
|
|
*
|
|
* The policy parameter controls how the relay handles sponsored transactions:
|
|
* - `'sign-only'`: Relay co-signs the transaction and returns it to the client transport, which then broadcasts it via the default transport
|
|
* - `'sign-and-broadcast'`: Relay co-signs and broadcasts the transaction directly
|
|
*
|
|
* @param defaultTransport - The default transport to use.
|
|
* @param relayTransport - The relay transport to use.
|
|
* @param parameters - Configuration parameters.
|
|
* @returns A relay transport.
|
|
*/
|
|
export function withRelay(
|
|
defaultTransport: Transport,
|
|
relayTransport: Transport,
|
|
parameters?: withRelay.Parameters,
|
|
): withRelay.ReturnValue {
|
|
const { policy = 'sign-only' } = parameters ?? {}
|
|
|
|
return (config) => {
|
|
const transport_default = defaultTransport(config)
|
|
const transport_relay = relayTransport(config)
|
|
|
|
return createTransport({
|
|
key: withRelay.type,
|
|
name: 'Relay Proxy',
|
|
async request({ method, params }, options) {
|
|
if (method === 'eth_fillTransaction')
|
|
return transport_relay.request({ method, params }, options) as never
|
|
|
|
if (
|
|
method === 'eth_sendRawTransactionSync' ||
|
|
method === 'eth_sendRawTransaction'
|
|
) {
|
|
const serialized = (params as any)[0] as `0x76${string}`
|
|
const transaction = Transaction.deserialize(serialized)
|
|
|
|
// Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
|
|
// signature until the relay co-signs the transaction.
|
|
if (transaction.feePayerSignature === null) {
|
|
// For 'sign-and-broadcast', relay signs and broadcasts
|
|
if (policy === 'sign-and-broadcast')
|
|
return transport_relay.request(
|
|
{ method, params },
|
|
options,
|
|
) as never
|
|
|
|
// For 'sign-only', request signature from relay using eth_signRawTransaction
|
|
{
|
|
// Request signature from relay using eth_signRawTransaction
|
|
const signedTransaction = await transport_relay.request(
|
|
{
|
|
method: 'eth_signRawTransaction',
|
|
params: [serialized],
|
|
},
|
|
options,
|
|
)
|
|
|
|
// Broadcast the signed transaction via the default transport
|
|
return transport_default.request(
|
|
{ method, params: [signedTransaction] },
|
|
options,
|
|
) as never
|
|
}
|
|
}
|
|
}
|
|
|
|
return (await transport_default.request(
|
|
{ method, params },
|
|
options,
|
|
)) as never
|
|
},
|
|
type: withRelay.type,
|
|
})
|
|
}
|
|
}
|
|
|
|
export declare namespace withRelay {
|
|
export const type = 'relay'
|
|
|
|
export type Parameters = RelayProxyParameters
|
|
|
|
export type ReturnValue = Relay
|
|
}
|
|
|
|
/** @deprecated Use `withRelay` instead. */
|
|
export function withFeePayer(
|
|
defaultTransport: Transport,
|
|
relayTransport: Transport,
|
|
parameters?: withFeePayer.Parameters,
|
|
): withFeePayer.ReturnValue {
|
|
const { policy = 'sign-only' } = parameters ?? {}
|
|
|
|
return (config) => {
|
|
const transport_default = defaultTransport(config)
|
|
const transport_relay = relayTransport(config)
|
|
|
|
return createTransport({
|
|
key: withFeePayer.type,
|
|
name: 'Relay Proxy',
|
|
async request({ method, params }, options) {
|
|
if (method === 'eth_fillTransaction') {
|
|
const request = (params as readonly unknown[] | undefined)?.[0]
|
|
if (
|
|
request &&
|
|
typeof request === 'object' &&
|
|
'feePayer' in request &&
|
|
request.feePayer === true
|
|
)
|
|
return transport_relay.request({ method, params }, options) as never
|
|
}
|
|
if (
|
|
method === 'eth_sendRawTransactionSync' ||
|
|
method === 'eth_sendRawTransaction'
|
|
) {
|
|
const serialized = (params as any)[0] as `0x76${string}`
|
|
const transaction = Transaction.deserialize(serialized)
|
|
|
|
// Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
|
|
// signature until the relay co-signs the transaction.
|
|
if (transaction.feePayerSignature === null) {
|
|
// For 'sign-and-broadcast', relay signs and broadcasts
|
|
if (policy === 'sign-and-broadcast')
|
|
return transport_relay.request(
|
|
{ method, params },
|
|
options,
|
|
) as never
|
|
|
|
// For 'sign-only', request signature from relay using eth_signRawTransaction
|
|
{
|
|
// Request signature from relay using eth_signRawTransaction
|
|
const signedTransaction = await transport_relay.request(
|
|
{
|
|
method: 'eth_signRawTransaction',
|
|
params: [serialized],
|
|
},
|
|
options,
|
|
)
|
|
|
|
// Broadcast the signed transaction via the default transport
|
|
return transport_default.request(
|
|
{ method, params: [signedTransaction] },
|
|
options,
|
|
) as never
|
|
}
|
|
}
|
|
}
|
|
return (await transport_default.request(
|
|
{ method, params },
|
|
options,
|
|
)) as never
|
|
},
|
|
type: withFeePayer.type,
|
|
})
|
|
}
|
|
}
|
|
|
|
export declare namespace withFeePayer {
|
|
export const type = 'feePayer'
|
|
|
|
export type Parameters = {
|
|
/** Policy for how the fee payer should handle transactions. Defaults to `'sign-only'`. */
|
|
policy?: 'sign-only' | 'sign-and-broadcast' | undefined
|
|
}
|
|
|
|
export type ReturnValue = FeePayer
|
|
}
|
|
|
|
/**
|
|
* Creates a transport that instruments a compatibility layer for
|
|
* `wallet_` RPC actions (`sendCalls`, `getCallsStatus`, etc).
|
|
*
|
|
* @param transport - Transport to wrap.
|
|
* @returns Transport.
|
|
*/
|
|
export function walletNamespaceCompat(
|
|
transport: Transport,
|
|
options: walletNamespaceCompat.Parameters,
|
|
): Transport {
|
|
const { account } = options
|
|
|
|
const sendCallsMagic = Hash.keccak256(Hex.fromString('TEMPO_5792'))
|
|
|
|
return (options) => {
|
|
const t = transport(options)
|
|
|
|
const chain = options.chain as Chain & ChainConfig
|
|
|
|
return {
|
|
...t,
|
|
async request(args: never) {
|
|
const request = RpcRequest.from(args)
|
|
|
|
const client = createClient({
|
|
chain,
|
|
transport,
|
|
})
|
|
|
|
if (request.method === 'wallet_sendCalls') {
|
|
const params = request.params[0] ?? {}
|
|
const { capabilities, chainId, from } = params
|
|
const { sync, ...properties } = capabilities ?? {}
|
|
|
|
if (!chainId) throw new Provider.UnsupportedChainIdError()
|
|
if (Number(chainId) !== client.chain.id)
|
|
throw new Provider.UnsupportedChainIdError()
|
|
if (from && !Address.isEqual(from, account.address))
|
|
throw new Provider.DisconnectedError()
|
|
|
|
const calls = (params.calls ?? []).map((call) => ({
|
|
to: call.to,
|
|
value: call.value ? BigInt(call.value) : undefined,
|
|
data: call.data,
|
|
}))
|
|
|
|
const hash = await (async () => {
|
|
if (!sync)
|
|
return sendTransaction(client, {
|
|
account,
|
|
...(properties ? properties : {}),
|
|
calls,
|
|
})
|
|
|
|
const { transactionHash } = await sendTransactionSync(client, {
|
|
account,
|
|
...(properties ? properties : {}),
|
|
calls,
|
|
})
|
|
return transactionHash
|
|
})()
|
|
|
|
const id = Hex.concat(hash, Hex.padLeft(chainId, 32), sendCallsMagic)
|
|
|
|
return {
|
|
capabilities: { sync },
|
|
id,
|
|
}
|
|
}
|
|
|
|
if (request.method === 'wallet_getCallsStatus') {
|
|
const [id] = request.params ?? []
|
|
if (!id) throw new Error('`id` not found')
|
|
if (!id.endsWith(sendCallsMagic.slice(2)))
|
|
throw new Error('`id` not supported')
|
|
Hex.assert(id)
|
|
|
|
const hash = Hex.slice(id, 0, 32)
|
|
const chainId = Hex.slice(id, 32, 64)
|
|
|
|
const receipt = await getTransactionReceipt(client, { hash })
|
|
return {
|
|
atomic: true,
|
|
chainId: Number(chainId),
|
|
id,
|
|
receipts: [receipt],
|
|
status: receipt.status === 'success' ? 200 : 500,
|
|
version: '2.0.0',
|
|
}
|
|
}
|
|
|
|
return t.request(args)
|
|
},
|
|
} as never
|
|
}
|
|
}
|
|
|
|
export declare namespace walletNamespaceCompat {
|
|
export type Parameters = {
|
|
account: LocalAccount
|
|
}
|
|
}
|