Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
/.idea/
/packages/sdk/dist/
/packages/accounts/dist/
/yarn-error.log
7 changes: 5 additions & 2 deletions packages/accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"clean": "rm -rf dist"
},
"version": "0.1.0",
"main": "index.js",
"license": "MIT"
"main": "./dist/index.js",
"license": "MIT",
"dependencies": {
"permissionless": "^0.3.2"
}
}
117 changes: 117 additions & 0 deletions packages/accounts/src/MultiChainSmartAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SmartAccount } from 'viem/account-abstraction'
import { Call, Hex, LocalAccount, } from 'viem'
import {
asCall,
BaseMultichainSmartAccount,
FunctionCall,
ICrossChainSdk,
MultichainBundlerManager,
UserOperation
} from '@eil-protocol/sdk'
import { toSimpleSmartAccount } from 'permissionless/accounts'

/**
* a simple MultichainAccount. using SimpleSmartAccount on each chain.
* Note that the signature method called for each SimpleSmartAccount instead separately.
* Also, its encoding can't support dynamic calls.
*/
export class MultiChainSmartAccount extends BaseMultichainSmartAccount {

accounts: Map<bigint, SmartAccount> = new Map()

/**
* Creates a new MultiChainSmartAccount instance
* @param owner - The account owner used for multichain signing. Must belong to the same owner across all chains.
* @param sdk - the sdk configuration to read supported network
* @param accounts - Array of {@link SmartAccount} instances for different chains (optional).
* @notice The owner must have signing capabilities for all chains.
* @dev Note that SimpleAccount API doesn't expose the owner directly.
*/
protected constructor (
readonly owner: LocalAccount,
sdk: ICrossChainSdk,
accounts: SmartAccount[]
) {

const bundlerManager = new MultichainBundlerManager(sdk.getNetworkEnv().input.chainInfos)
super(bundlerManager)
this.addAccounts(accounts)
}

static async create (
owner: LocalAccount,
sdk: ICrossChainSdk,
accounts?: SmartAccount[]
): Promise<MultiChainSmartAccount> {
const networkEnv = sdk.getNetworkEnv()
if (accounts == null) {
accounts = []
for (const chain of networkEnv.input.chainInfos) {
let client = networkEnv.chains.clientOn(chain.chainId)
const entryPointAddress = networkEnv.entrypoints.addressOn(chain.chainId)

const factoryAddress = '0x2862B77afcF4405e766328E697E0236b9974b8fa' //ep9 simpleAccount factory
const account: SmartAccount = await toSimpleSmartAccount({
owner,
client,
entryPoint: {
address: entryPointAddress,
version: '0.8'
},
factoryAddress
})
accounts.push(account)
}
}
return new MultiChainSmartAccount(
owner,
sdk,
accounts
)
}

//add chain-specific instances of the account.
//TODO: currently needs viem "SmartAccount" for each chain. better unify it
// (some methods are chain-specific, like "getNonce". others (like encode) are not.
protected addAccounts (accounts: SmartAccount[]) {
for (const account of accounts) {
const chainId = BigInt(account.client.chain?.id!)
if (this.accounts.has(chainId)) {
throw new Error(`Account object already exists for chainId: ${chainId}`)
}
this.accounts.set(chainId, account)
}
}

hasAddress (chainId: bigint): boolean {
return this.accounts.has(chainId)
}

contractOn (chainId: bigint): SmartAccount {
if (!this.accounts.has(chainId)) {
throw new Error(`No account found for chainId: ${chainId}`)
}
return this.accounts.get(chainId)!
}

async signUserOps (userOps: UserOperation[]): Promise<UserOperation[]> {
// naive implementation: sign using each per-chain account.
const result: UserOperation[] = []
for (const userOp of userOps) {
const chainId = userOp.chainId
if (chainId == null) {
throw new Error(`signUserOps: can only sign userOps with chain`)
}
const account = this.contractOn(chainId)
const signature = await account.signUserOperation(userOp as any)
result.push({ ...userOp, signature })
}
return result
}

async encodeCalls (chainId: bigint, calls: Array<Call | FunctionCall>): Promise<Hex> {
const plainCalls: Call[] = calls.map(call => asCall(chainId, call))
return this.contractOn(chainId).encodeCalls(plainCalls)
}
}

3 changes: 2 additions & 1 deletion packages/accounts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
console.log('accounts')
export * from './ambire/AmbireMultiChainSmartAccount.js'
export * from './MultiChainSmartAccount.js'
28 changes: 18 additions & 10 deletions packages/sdk/src/sdk/CrossChainSdk.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import { CrossChainConfig } from './config/index.js'
import { ICrossChainBuilder, ICrossChainSdk, IJsonRpcProvider } from './types/index.js'
import { CrossChainBuilder, InternalConfig } from './builder/index.js'
import { IMultiChainSmartAccount } from './account/index.js'
import { CrossChainConfig, defaultCrossChainConfig } from './config/index.js'
import { AddressPerChain, ICrossChainBuilder, ICrossChainSdk, MultichainToken } from './types/index.js'
import { CrossChainBuilder, NetworkEnvironment } from './builder/index.js'

/**
* This class is the main component for building cross-chain actions.
* It holds the configuration and is used to create the {@link BatchBuilder}.
*/
export class CrossChainSdk implements ICrossChainSdk {

config: InternalConfig
networkEnv: NetworkEnvironment

constructor (
readonly account: IMultiChainSmartAccount,
config: CrossChainConfig,
readonly walletProvider: IJsonRpcProvider | undefined = undefined
config: CrossChainConfig = defaultCrossChainConfig
) {
this.config = new InternalConfig(config)
this.networkEnv = new NetworkEnvironment(config)
}

/**
* create a builder for a cross-chain operation
*/
createBuilder (): ICrossChainBuilder {
return new CrossChainBuilder(this.config, this.account)
return new CrossChainBuilder(this.networkEnv)
}

/**
* create a MultichainToken with the given deployment addresses
*/
createToken (name: string, deployments: AddressPerChain): MultichainToken {
return new MultichainToken(name, this.networkEnv.chains, deployments)
}

getNetworkEnv (): NetworkEnvironment {
return this.networkEnv
}
}

2 changes: 1 addition & 1 deletion packages/sdk/src/sdk/actions/VoucherRequestAction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Address, Call } from 'viem'

import { BaseAction, BatchBuilder, FunctionCall, SdkVoucherRequest, toAddress } from '../index.js'
import { NATIVE_ETH } from '../types/Constants.js'
import { NATIVE_ETH } from '../types/index.js'

/**
* The internal class defining an action to lock the user deposit for the specified {@link SdkVoucherRequest}.
Expand Down
41 changes: 24 additions & 17 deletions packages/sdk/src/sdk/builder/BatchBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Address, Hex, PrivateKeyAccount, publicActions } from 'viem'
import { Address, Hex, hexToBigInt, PrivateKeyAccount, publicActions } from 'viem'
import { BaseAction, FunctionCallAction, prepareCallWithRuntimeVars, VoucherRequestAction } from '../actions/index.js'
import { CrossChainBuilder } from './CrossChainBuilder.js'
import { CrossChainVoucherCoordinator } from './CrossChainVoucherCoordinator.js'
import { InternalConfig } from './InternalConfig.js'
import { NetworkEnvironment } from './NetworkEnvironment.js'
import {
ICrossChainBuilder,
InternalVoucherInfo,
isCall,
isValidAddress,
Expand All @@ -15,9 +16,8 @@ import {
} from '../types/index.js'
import { appendPaymasterSignature, getUserOpHash } from '../index.js'
import { assert } from '../sdkUtils/SdkUtils.js'
import { IMultiChainSmartAccount } from '../account/index.js'
import { Asset } from '../../contractTypes/Asset.js'
import { abiEncodePaymasterData } from '../../utils/index.js'
import { abiEncodePaymasterData, nowSeconds } from '../../utils/index.js'

/**
* Return the minimum amount for an asset.
Expand Down Expand Up @@ -56,14 +56,18 @@ export class BatchBuilder {
private _vars: Set<string> = new Set()

constructor (
private readonly parentBuilder: ICrossChainBuilder,
private readonly ephemeralSigner: PrivateKeyAccount,
private readonly coordinator: CrossChainVoucherCoordinator,
readonly config: InternalConfig,
private readonly smartAccount: IMultiChainSmartAccount,
readonly config: NetworkEnvironment,
readonly paymaster: `0x${string}`,
readonly chainId: bigint
) {}

endBatch (): ICrossChainBuilder {
return this.parentBuilder
}

/**
* Add a new dynamic runtime variable to the batch.
* Variables can be used to store values that are not known at the time of batch creation.
Expand Down Expand Up @@ -127,7 +131,7 @@ export class BatchBuilder {
} else {
assert(req.sourceChainId == this.chainId, `Voucher request sourceChainId ${req.sourceChainId} does not match batch chainId ${this.chainId}`)
}
assert(!this.coordinator.has(req), `Voucher request ${req} already exists in this BatchBuilder`)
assert(!this.coordinator.has(req), `Voucher request ${req.ref} already exists in this BatchBuilder`)

req.tokens.forEach(token => {
if (!isValidAddress(req.sourceChainId!, token.token)) {
Expand All @@ -138,7 +142,7 @@ export class BatchBuilder {
}
})

this.coordinator.set(req, {
this.coordinator.set({
voucher: req,
sourceBatch: this,
})
Expand All @@ -155,13 +159,14 @@ export class BatchBuilder {
/**
* Use the {@link SdkVoucherRequest} created in an earlier batch to move tokens to this chain.
*/
useVoucher (voucher: SdkVoucherRequest): this {
useVoucher (refId: string): this {
this.assertNotBuilt()
const internalVoucherInfo = this.coordinator.getVoucherInternalInfo(voucher)
assert(internalVoucherInfo != null, `Voucher request ${voucher} not found in action builder`)
const internalVoucherInfo = this.coordinator.getVoucherInternalInfo(refId)
const voucher = internalVoucherInfo?.voucher!
assert(internalVoucherInfo != null, `Voucher request ${refId} not found in action builder`)
assert(this.userOpOverrides?.paymaster == null && this.userOpOverrides?.paymasterData == null,
`Cannot override paymaster or paymasterData in a batch that uses vouchers.`)
assert(internalVoucherInfo.destBatch == undefined, `Voucher request ${voucher} already used`)
assert(internalVoucherInfo.destBatch == undefined, `Voucher request ${refId} already used`)
assert(voucher.destinationChainId == this.chainId,
`Voucher request is for chain ${voucher.destinationChainId}, but batch is for chain ${this.chainId}`)
internalVoucherInfo.destBatch = this
Expand All @@ -179,7 +184,7 @@ export class BatchBuilder {
for (const v of allVoucherRequests) {
if (v.destinationChainId === this.chainId) {
added = true
this.useVoucher(v)
this.useVoucher(v.ref)
}
}
assert(added, `No voucher requests found for chain ${this.chainId}`)
Expand All @@ -194,7 +199,8 @@ export class BatchBuilder {
async createUserOp (): Promise<UserOperation> {
const chainId = this.chainId

const smartAccount = this.smartAccount.contractOn(chainId)
const mcAccount = this.parentBuilder.getAccount()
const smartAccount = mcAccount.contractOn(chainId)
const allCalls = await Promise.all(this.actions.map((action) => {
return action.encodeCall(this)
}))
Expand All @@ -209,14 +215,15 @@ export class BatchBuilder {
smartAccount.getAddress(),
smartAccount.getNonce(),
smartAccount.getFactoryArgs(),
calls.length == 0 ? '0x' : this.smartAccount.encodeCalls(chainId, calls)
calls.length == 0 ? '0x' : mcAccount.encodeCalls(chainId, calls)
])

const nonce1 = BigInt(nowSeconds()) << 64n
const { maxFeePerGas, maxPriorityFeePerGas } = await smartAccount.client.extend(publicActions).estimateFeesPerGas()
let userOp = {
chainId,
sender,
nonce,
nonce: nonce1,
factory,
factoryData,
callData,
Expand Down Expand Up @@ -312,7 +319,7 @@ export class BatchBuilder {
}

getVoucherInternalInfo (voucher: SdkVoucherRequest): InternalVoucherInfo | undefined {
return this.coordinator.getVoucherInternalInfo(voucher)
return this.coordinator.getVoucherInternalInfo(voucher.ref)
}

getOutVoucherRequests (): SdkVoucherRequest[] {
Expand Down
Loading