Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"npm-run-all": "^4.1.5",
"tsup": "^8.3.5"
},
"packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
"packageManager": "pnpm@10.20.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { EIP1474Methods, PublicClient } from "viem"
import { SmartAccount } from 'viem/account-abstraction'
import { ProviderRequestMethod } from '../../provider'
import { SmartAccountDeploymentError, WaitForSmartAccountDeploymentError } from './errors'
import { isSmartAccountDeployed } from './is-smart-account-deployed'
import { waitForSmartAccountDeployment, WaitForSmartAccountDeploymentErrorType } from './wait-for-smart-account-deployment'

/**
* Ensures that a smart account is deployed on the blockchain.
*
* This function checks if the smart account is already deployed, and if not,
* it initiates deployment by sending a minimal transaction to the account address.
* The deployment process uses a simple transaction with zero value and empty data
* to trigger the account's creation through the account abstraction infrastructure.
*
* @param smartAccount - The smart account instance to deploy. This should be a
* configured SmartAccount that can provide its address and
* deployment information.
* @param client - The public client instance used to interact with the blockchain.
* This client is used to check deployment status and wait for
* deployment confirmation.
* @param eth_sendTransaction - The provider method for sending transactions.
* This is used to send the deployment transaction
* that triggers smart account creation.
*
* @returns A Promise that resolves when the smart account is confirmed to be
* deployed on the blockchain.
*
* @throws {WaitForSmartAccountDeploymentErrorType} When there's an error during
* the deployment waiting process, including timeout errors or deployment
* status checking failures.
* @throws {SmartAccountDeploymentError} When deployment fails for any other
* reason, such as transaction sending failures or unexpected errors.
*
* @example
* ```typescript
* import { createPublicClient, http } from 'viem'
* import { mainnet } from 'viem/chains'
*
* const client = createPublicClient({
* chain: mainnet,
* transport: http()
* })
*
* const smartAccount = // ... your configured smart account
* const sendTransaction = // ... your provider's eth_sendTransaction method
*
* try {
* await ensureSmartAccountIsDeployed(smartAccount, client, sendTransaction)
* console.log('Smart account is now deployed')
* } catch (error) {
* console.error('Failed to deploy smart account:', error)
* }
* ```
*/
export const ensureSmartAccountIsDeployed = async (
smartAccount: SmartAccount,
client: PublicClient,
eth_sendTransaction: ProviderRequestMethod<EIP1474Methods, 'eth_sendTransaction'>,
): Promise<void> => {
try {
const isDeployed = await isSmartAccountDeployed(client, smartAccount)

if (isDeployed) {
return
}

// Send a minimal transaction with zero value and empty data
// to trigger the account abstraction deployment process
await eth_sendTransaction([{
to: await smartAccount.getAddress(),
value: '0x0',
data: '0x',
}])

// We check the account code directly instead of using the user-op hash
// to support privacy-enabled bundlers that require signatures for read calls.
// Since the smart account isn't deployed yet, the bundler can't verify signatures
// using the smart account's ERC-4337 signature verification logic.
await waitForSmartAccountDeployment(client, { smartAccount })
} catch (error) {
if (error instanceof WaitForSmartAccountDeploymentError) {
throw error
}

throw new SmartAccountDeploymentError(
'Failed to deploy smart account',
{ cause: error }
)
}
}
16 changes: 16 additions & 0 deletions packages/connector/src/account/deployment/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GianoError, GianoErrorOptions } from '../../giano-error'

export class SmartAccountDeploymentError extends GianoError {}

export class WaitForSmartAccountDeploymentError extends SmartAccountDeploymentError {}

export class WaitForSmartAccountDeploymentTimeoutError extends WaitForSmartAccountDeploymentError {
constructor(
/** The timeout value in milliseconds. */
readonly timeout: number,
options?: GianoErrorOptions,
) {
const message = `Timeout waiting for smart account deployment after ${timeout}ms`
super(message, options)
}
}
4 changes: 4 additions & 0 deletions packages/connector/src/account/deployment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './ensure-smart-account-is-deployed'
export * from './errors'
export * from './is-smart-account-deployed'
export * from './wait-for-smart-account-deployment'
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { PublicClient } from 'viem'
import { SmartAccount } from 'viem/account-abstraction'

/**
* Checks whether a smart account is deployed on the blockchain.
*
* This function determines deployment status by examining the bytecode at the
* smart account's address. A smart account is considered deployed if there is
* non-empty bytecode at its address (anything other than undefined or '0x').
*
* @param client - The public client instance used to interact with the blockchain.
* This client is used to fetch the bytecode at the smart account's
* address.
* @param smartAccount - The smart account instance to check for deployment.
* The function will get the account's address and check
* if bytecode exists at that address.
*
* @returns A Promise that resolves to `true` if the smart account is deployed
* (has bytecode at its address), `false` otherwise.
*
* @throws May throw errors from the underlying client operations, such as:
* - Network connectivity issues
* - Invalid address errors
* - RPC provider errors
*
* @example
* ```typescript
* import { createPublicClient, http } from 'viem'
* import { mainnet } from 'viem/chains'
*
* const client = createPublicClient({
* chain: mainnet,
* transport: http()
* })
*
* const smartAccount = // ... your configured smart account
*
* const isDeployed = await isSmartAccountDeployed(client, smartAccount)
*
* if (isDeployed) {
* console.log('Smart account is already deployed')
* } else {
* console.log('Smart account needs to be deployed')
* }
* ```
*/
export const isSmartAccountDeployed = async (
client: PublicClient,
smartAccount: SmartAccount,
): Promise<boolean> => {
const address = await smartAccount.getAddress()
const code = await client!.getCode({ address })

return code !== undefined && code !== '0x'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { PublicClient } from 'viem'
import { SmartAccount } from 'viem/account-abstraction'
import {
WaitForSmartAccountDeploymentError,
WaitForSmartAccountDeploymentTimeoutError,
} from './errors'
import { isSmartAccountDeployed } from './is-smart-account-deployed'

const DEFAULT_TIMEOUT_MS = 120_000

export type WaitForSmartAccountDeploymentParameters = {
/** The smart account to wait for. */
smartAccount: SmartAccount
/**
* Polling frequency (in ms). Defaults to the client's pollingInterval config.
* @default client.pollingInterval
*/
pollingInterval?: number | undefined
/**
* Optional timeout (in ms) to wait before stopping polling.
* Defaults to {@link DEFAULT_TIMEOUT_MS}.
*/
timeout?: number | undefined
}

/**
* Waits for a smart account to be deployed on the blockchain using polling.
*
* This function continuously polls the blockchain to check if the smart account
* has been deployed by examining its bytecode. It will poll immediately upon
* calling, then continue polling at the specified interval until either the
* account is deployed, an error occurs, or the timeout is reached.
*
* @param client - The public client instance used to interact with the blockchain.
* The client's pollingInterval is used as the default polling frequency.
* @param parameters - Configuration object containing the smart account to wait for
* and optional polling/timeout settings.
*
* @returns A Promise that resolves when the smart account is confirmed to be
* deployed on the blockchain (has bytecode at its address).
*
* @throws {WaitForSmartAccountDeploymentTimeoutError} When the specified timeout
* is reached before the smart account is deployed.
* @throws {WaitForSmartAccountDeploymentError} When there's an error checking
* the deployment status during the polling process.
*
* @example
* ```typescript
* import { createPublicClient, http } from 'viem'
* import { mainnet } from 'viem/chains'
*
* const client = createPublicClient({
* chain: mainnet,
* transport: http()
* })
*
* const smartAccount = // ... your configured smart account
*
* try {
* // Wait with default settings (default timeout, client's polling interval)
* await waitForSmartAccountDeployment(client, { smartAccount })
* console.log('Smart account deployment confirmed')
*
* // Wait with custom settings
* await waitForSmartAccountDeployment(client, {
* smartAccount,
* pollingInterval: 1000, // Check every second
* timeout: 60000 // 1 minute timeout
* })
* } catch (error) {
* if (error instanceof WaitForSmartAccountDeploymentTimeoutError) {
* console.error('Deployment timed out after', error.timeout, 'ms')
* } else {
* console.error('Failed to wait for deployment:', error)
* }
* }
* ```
*/
export function waitForSmartAccountDeployment(
client: PublicClient,
parameters: WaitForSmartAccountDeploymentParameters,
): Promise<void> {
const {
smartAccount,
pollingInterval = client.pollingInterval,
timeout = DEFAULT_TIMEOUT_MS,
} = parameters

let timeoutId: NodeJS.Timeout | undefined
let intervalId: NodeJS.Timeout | undefined

const cleanup = () => {
if (timeoutId) clearTimeout(timeoutId)
if (intervalId) clearInterval(intervalId)
}

return new Promise((resolve, reject) => {
if (timeout) {
timeoutId = setTimeout(() => {
cleanup()
reject(new WaitForSmartAccountDeploymentTimeoutError(timeout))
}, timeout)
}

const poll = async () => {
try {
const isDeployed = await isSmartAccountDeployed(client, smartAccount)

if (isDeployed) {
cleanup()
resolve()
return
}
} catch (error) {
cleanup()
reject(new WaitForSmartAccountDeploymentError(
'Failed to check smart account deployment status',
{ cause: error }
))
}
}

poll()
intervalId = setInterval(poll, pollingInterval)
})
}

export type WaitForSmartAccountDeploymentErrorType =
| WaitForSmartAccountDeploymentError
| WaitForSmartAccountDeploymentTimeoutError
1 change: 1 addition & 0 deletions packages/connector/src/account/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './deployment'
export * from './get-web-authn-account'
export * from './toGianoSmartAccount'
22 changes: 16 additions & 6 deletions packages/connector/src/account/toGianoSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ import {
toHex,
} from 'viem';
import type { SmartAccount, SmartAccountImplementation, UserOperation, WebAuthnAccount } from 'viem/account-abstraction';
import { entryPoint07Abi } from 'viem/account-abstraction';
import { entryPoint07Address, getUserOperationHash, toSmartAccount } from 'viem/account-abstraction';
import { getUserOperationHash, toSmartAccount } from 'viem/account-abstraction';
import { readContract } from 'viem/actions';
import {
GianoEntryPointAbi,
GianoEntryPointAddress,
GianoEntryPointVersion,
} from '../giano-entry-point'

export type ToGianoSmartAccountParameters = {
address?: Address | undefined;
Expand All @@ -36,7 +40,11 @@ export type ToGianoSmartAccountParameters = {
export type ToGianoSmartAccountReturnType = Prettify<SmartAccount<GianoSmartAccountImplementation>>;

export type GianoSmartAccountImplementation = Assign<
SmartAccountImplementation<typeof entryPoint07Abi, '0.7', { abi: typeof abi; factory: { abi: typeof factoryAbi; address: Address } }>,
SmartAccountImplementation<
typeof GianoEntryPointAbi,
GianoEntryPointVersion,
{ abi: typeof abi; factory: { abi: typeof factoryAbi; address: Address } }
>,
{
decodeCalls: NonNullable<SmartAccountImplementation['decodeCalls']>;
sign: NonNullable<SmartAccountImplementation['sign']>;
Expand Down Expand Up @@ -66,9 +74,9 @@ export async function toGianoSmartAccount(parameters: ToGianoSmartAccountParamet
let address = parameters.address;

const entryPoint = {
abi: entryPoint07Abi,
address: entryPoint07Address,
version: '0.7',
abi: GianoEntryPointAbi,
address: GianoEntryPointAddress,
version: GianoEntryPointVersion,
} as const;
const factory = {
abi: factoryAbi,
Expand Down Expand Up @@ -268,6 +276,8 @@ export async function toGianoSmartAccount(parameters: ToGianoSmartAccountParamet
});
}

export type GianoSmartAccount = Awaited<ReturnType<typeof toGianoSmartAccount>>

/////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
/////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
8 changes: 7 additions & 1 deletion packages/connector/src/giano-entry-point.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { entryPoint07Address, EntryPointVersion } from 'viem/account-abstraction'
import {
entryPoint07Abi,
entryPoint07Address,
EntryPointVersion,
} from 'viem/account-abstraction'

export const GianoEntryPointVersion = '0.7' satisfies EntryPointVersion
export type GianoEntryPointVersion = typeof GianoEntryPointVersion

export const GianoEntryPointAbi = entryPoint07Abi

export const GianoEntryPointAddress = entryPoint07Address
export type GianoEntryPointAddress = typeof GianoEntryPointAddress
Loading