-
Notifications
You must be signed in to change notification settings - Fork 248
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #91 from base-org/wilson/writeContractDeposit
writeContractDeposit
- Loading branch information
Showing
8 changed files
with
301 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"op-viem": patch | ||
--- | ||
|
||
Add writeContractDeposit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { optimismPortalABI } from '@eth-optimism/contracts-ts' | ||
import { decodeEventLog, encodeFunctionData } from 'viem' | ||
import { mine } from 'viem/actions' | ||
import { expect, test } from 'vitest' | ||
import { erc721ABI } from 'wagmi' | ||
import { accounts } from '../../../_test/constants.js' | ||
import { publicClient, testClient, walletClient } from '../../../_test/utils.js' | ||
import { base } from '../../../chains/base.js' | ||
import { parseOpaqueData } from '../../../utils/getArgsFromTransactionDepositedOpaqueData.js' | ||
import { writeContractDeposit } from './writeContractDeposit.js' | ||
|
||
test('default', async () => { | ||
const functionName = 'approve' | ||
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n] | ||
const l2GasLimit = 100000n | ||
const hash = await writeContractDeposit(walletClient, { | ||
abi: erc721ABI, | ||
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b', | ||
functionName, | ||
args, | ||
account: accounts[0].address, | ||
l2GasLimit, | ||
l2Chain: base, | ||
}) | ||
await mine(testClient, { blocks: 1 }) | ||
|
||
const txReceipt = await publicClient.getTransactionReceipt({ hash }) | ||
expect(txReceipt.status).toEqual('success') | ||
const depositEvents = [] | ||
for (const l of txReceipt.logs) { | ||
try { | ||
const event = decodeEventLog({ | ||
abi: optimismPortalABI, | ||
data: l.data, | ||
topics: l.topics, | ||
}) | ||
if (event.eventName === 'TransactionDeposited') { | ||
depositEvents.push({ event, logIndex: l.logIndex }) | ||
} | ||
} catch {} | ||
} | ||
const parsedOpaqueData = parseOpaqueData(depositEvents[0].event.args.opaqueData) | ||
expect(BigInt(parsedOpaqueData.mint)).toEqual(0n) | ||
expect(BigInt(parsedOpaqueData.value)).toEqual(0n) | ||
expect(BigInt(parsedOpaqueData.gas)).toEqual(l2GasLimit) | ||
expect(parsedOpaqueData?.data).toEqual(encodeFunctionData({ | ||
abi: erc721ABI, | ||
args, | ||
functionName, | ||
})) | ||
}) | ||
|
||
test('throws error if strict = true and account is smart contract wallet', async () => { | ||
const scw_address = '0x2De5Aad1bD26ec3fc4F64E95068c02D84Df072C6' | ||
await testClient.impersonateAccount({ | ||
address: scw_address, | ||
}) | ||
const functionName = 'approve' | ||
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n] | ||
const l2GasLimit = 100000n | ||
expect(() => | ||
writeContractDeposit(walletClient, { | ||
abi: erc721ABI, | ||
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b', | ||
functionName, | ||
args, | ||
account: scw_address, | ||
l2GasLimit, | ||
l2Chain: base, | ||
}) | ||
).rejects.toThrowError( | ||
'Calling depositTransaction from a smart contract can have unexpected results, see https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#address-aliasing. Set `strict` to false to disable this check.', | ||
) | ||
}) | ||
|
||
test('allows smart contract wallet if strict = false', async () => { | ||
const scw_address = '0x2De5Aad1bD26ec3fc4F64E95068c02D84Df072C6' | ||
await testClient.impersonateAccount({ | ||
address: scw_address, | ||
}) | ||
await testClient.setBalance({ | ||
address: scw_address, | ||
value: 10n ** 22n, | ||
}) | ||
const functionName = 'approve' | ||
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n] | ||
const l2GasLimit = 100000n | ||
expect( | ||
await writeContractDeposit(walletClient, { | ||
abi: erc721ABI, | ||
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b', | ||
functionName, | ||
args, | ||
account: scw_address, | ||
l2GasLimit, | ||
l2Chain: base, | ||
strict: false, | ||
}), | ||
).toBeDefined() | ||
}) | ||
|
||
test('throws error if no account passed', async () => { | ||
const functionName = 'approve' | ||
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n] | ||
const l2GasLimit = 100000n | ||
expect(() => | ||
// @ts-expect-error | ||
writeContractDeposit(walletClient, { | ||
abi: erc721ABI, | ||
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b', | ||
functionName, | ||
args, | ||
l2GasLimit, | ||
l2Chain: base, | ||
}) | ||
).rejects.toThrowError( | ||
'No account found', | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { | ||
type Abi, | ||
type Account, | ||
type Address, | ||
type Chain, | ||
encodeFunctionData, | ||
type EncodeFunctionDataParameters, | ||
type Transport, | ||
type WalletClient, | ||
type WriteContractParameters, | ||
type WriteContractReturnType, | ||
} from 'viem' | ||
import { getBytecode } from 'viem/actions' | ||
import { parseAccount } from 'viem/utils' | ||
import { OpStackL1Contract } from '../../../index.js' | ||
import type { GetL2Chain, L1ActionBaseType, ResolveChain } from '../../../types/l1Actions.js' | ||
import { writeDepositTransaction, type WriteDepositTransactionParameters } from './writeDepositTransaction.js' | ||
|
||
export type WriteContractDepositParameters< | ||
TAbi extends Abi | readonly unknown[] = Abi, | ||
TFunctionName extends string = string, | ||
TChain extends Chain | undefined = Chain, | ||
TAccount extends Account | undefined = Account | undefined, | ||
TChainOverride extends Chain | undefined = Chain | undefined, | ||
> = | ||
& { account: TAccount | Address; l2GasLimit: bigint; l2MsgValue?: bigint; strict?: boolean } | ||
& L1ActionBaseType<GetL2Chain<ResolveChain<TChain, TChainOverride>>, typeof OpStackL1Contract.OptimismPortal> | ||
& Omit< | ||
WriteContractParameters< | ||
TAbi, | ||
TFunctionName, | ||
TChain, | ||
TAccount, | ||
TChainOverride | ||
>, // NOTE(Wilson): | ||
// In the future we could possibly allow value to be passed, creating an L2 mint | ||
// as writeDepositTransaction does but I want to avoid for now as it complicates | ||
// simulating the L2 transaction that results from this call, as we have no to mock/simulate the L2 mint. | ||
'value' | 'account' | ||
> | ||
|
||
/** | ||
* A L1 -> L2 version of Viem's writeContract. Can be used to create an arbitrary L2 transaction from L1. | ||
* NOTE: If caller is a smart contract wallet, msg.sender on the L2 transaction will be an alias of the L1 address. | ||
* Must set `strict` = false to allow calling from smart contract wallet. | ||
* | ||
* @param parameters - {@link WriteContractDepositParameters} | ||
* @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} | ||
*/ | ||
export async function writeContractDeposit< | ||
TChain extends Chain | undefined, | ||
TAccount extends Account | undefined, | ||
const TAbi extends Abi | readonly unknown[], | ||
TFunctionName extends string, | ||
TChainOverride extends Chain | undefined, | ||
>( | ||
client: WalletClient<Transport, TChain, TAccount>, | ||
{ | ||
abi, | ||
account: account_ = client.account, | ||
address, | ||
args, | ||
functionName, | ||
l2GasLimit, | ||
l2MsgValue = 0n, | ||
l2Chain, | ||
optimismPortalAddress, | ||
strict = true, | ||
...request | ||
}: WriteContractDepositParameters< | ||
TAbi, | ||
TFunctionName, | ||
TChain, | ||
TAccount, | ||
TChainOverride | ||
>, | ||
): Promise<WriteContractReturnType> { | ||
const calldata = encodeFunctionData({ | ||
abi, | ||
args, | ||
functionName, | ||
} as unknown as EncodeFunctionDataParameters<TAbi, TFunctionName>) | ||
if (!account_) { | ||
throw new Error('No account found') | ||
} | ||
const account = parseAccount(account_) | ||
|
||
if (strict) { | ||
const code = await getBytecode(client, { address: account.address }) | ||
if (code) { | ||
throw new Error( | ||
'Calling depositTransaction from a smart contract can have unexpected results, see https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#address-aliasing. Set `strict` to false to disable this check.', | ||
) | ||
} | ||
} | ||
return writeDepositTransaction(client, { | ||
optimismPortalAddress, | ||
l2Chain, | ||
args: { gasLimit: l2GasLimit, to: address, data: calldata, value: l2MsgValue }, | ||
account, | ||
...request, | ||
} as unknown as WriteDepositTransactionParameters<TChain, TAccount, TChainOverride>) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { type Hex, size, slice } from 'viem' | ||
import type { TransactionDepositedEvent } from '../index.js' | ||
|
||
export type ParsedTransactionDepositedOpaqueData = { | ||
// TODO(Wilson): consider using mint in writeDepositContract is it is more | ||
// clear that this is the value that will be minted on L2 | ||
// this type could then maybe be called DepositTransactionArgs, though it would have this | ||
// additional mint field, when compared to the contract | ||
mint: Hex | ||
value: Hex | ||
gas: Hex | ||
isCreation: boolean | ||
data: Hex | ||
} | ||
|
||
/** | ||
* @description Returns the TransactionDeposited event and log index, if found, | ||
* from the transaction receipt | ||
* | ||
* @param {opaqueData} opaqueData from the TransactionDepositedEvent event args | ||
* @returns {ParsedTransactionDepositedOpaqueData} The data parsed into five fields | ||
*/ | ||
export function parseOpaqueData( | ||
opaqueData: TransactionDepositedEvent['args']['opaqueData'], | ||
): ParsedTransactionDepositedOpaqueData { | ||
let offset = 0 | ||
const mint = slice(opaqueData, offset, offset + 32) | ||
offset += 32 | ||
const value = slice(opaqueData, offset, offset + 32) | ||
offset += 32 | ||
const gas = slice(opaqueData, offset, offset + 8) | ||
offset += 8 | ||
const isCreation = BigInt(opaqueData[offset]) === 1n | ||
offset += 1 | ||
const data = | ||
// NOTE(Wilson): this is to deal with kind of odd behvior in slice | ||
// https://github.com/wagmi-dev/viem/blob/main/src/utils/data/slice.ts#L34 | ||
offset > size(opaqueData) - 1 | ||
? '0x' | ||
: slice(opaqueData, offset, opaqueData.length) | ||
return { | ||
mint, | ||
value, | ||
gas, | ||
isCreation, | ||
data, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters