Skip to content

Commit

Permalink
wip: checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Jan 1, 2025
1 parent 58618fb commit 5ea8d90
Show file tree
Hide file tree
Showing 15 changed files with 1,502 additions and 64 deletions.
148 changes: 92 additions & 56 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ catalog:
react: "^18.3.1"
react-dom: "^18.3.1"
typescript: "^5.5.4"
viem: "^2.21.51"
viem: "^2.22.1"
wagmi: "^2.12.30"
1 change: 1 addition & 0 deletions src/core/Porto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function create(config: Config | undefined = {}): Porto {
return createClient({
chain,
transport: (transports as Record<number, Transport>)[chain.id]!,
pollingInterval: 1_000,
})
},
get delegation() {
Expand Down
3 changes: 3 additions & 0 deletions src/core/internal/__snapshots__/provider.test.ts.snap

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions src/core/internal/delegatedAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { AbiFunction, P256, Secp256k1, Value } from 'ox'
import { privateKeyToAccount } from 'viem/accounts'
import { getBalance, readContract, setBalance } from 'viem/actions'
import {
type SignAuthorizationParameters,
signAuthorization,
} from 'viem/experimental'
import { describe, expect, test } from 'vitest'

import { porto } from '../../../test/src/config.js'
import * as DelegatedAccount from './delegatedAccount.js'
import { delegationAbi } from './generated.js'
import * as Key from './key.js'

const state = porto._internal.store.getState()
const client = state.client.extend(() => ({ mode: 'anvil' }))

async function setup(
parameters: {
delegate?: SignAuthorizationParameters['delegate'] | undefined
} = {},
) {
const { delegate } = parameters

const privateKey = Secp256k1.randomPrivateKey()
const eoa = privateKeyToAccount(privateKey)

await setBalance(client, {
address: eoa.address,
value: Value.fromEther('2'),
})

const authorization = await signAuthorization(client, {
account: eoa,
contractAddress: state.delegation,
delegate,
})

return { authorization, eoa }
}

describe('authorize', () => {
test('sender = EOA, keysToAuthorize = [P256], executor = EOA', async () => {
const { authorization, eoa } = await setup()

const key = Key.fromP256({
privateKey: P256.randomPrivateKey(),
role: 'admin',
})

const account = DelegatedAccount.from({
address: eoa.address,
keys: [key],
})

await DelegatedAccount.execute(client, {
account,
authorizationList: [authorization],
executor: eoa,
calls: [
{
data: AbiFunction.encodeData(
AbiFunction.fromAbi(delegationAbi, 'authorize'),
[Key.serialize(key)],
),
to: account.address,
},
],
})

const serializedKey = await readContract(client, {
abi: delegationAbi,
address: account.address,
functionName: 'keyAt',
args: [0n],
})

expect(Key.deserialize(serializedKey)).toEqual({ ...key, sign: undefined })
})
})

describe('execute', () => {
test.skip('sender = EOA, executor = JSON-RPC', async () => {
const { authorization, eoa } = await setup({ delegate: true })

const alice = privateKeyToAccount(Secp256k1.randomPrivateKey())
const bob = privateKeyToAccount(Secp256k1.randomPrivateKey())

const balances_before = await Promise.all([
getBalance(client, { address: eoa.address }),
getBalance(client, { address: alice.address }),
getBalance(client, { address: bob.address }),
])

expect(balances_before[0]).toEqual(Value.fromEther('2'))
expect(balances_before[1]).toEqual(Value.fromEther('0'))
expect(balances_before[2]).toEqual(Value.fromEther('0'))

await DelegatedAccount.execute(client, {
account: eoa,
authorizationList: [authorization],
executor: null,
calls: [
{ to: alice.address, value: Value.fromEther('1') },
{ to: bob.address, value: Value.fromEther('0.5') },
],
})

const balances_after = await Promise.all([
getBalance(client, { address: eoa.address }),
getBalance(client, { address: alice.address }),
getBalance(client, { address: bob.address }),
])

expect(balances_after[0]).not.toBeGreaterThan(
balances_before[0] - Value.fromEther('1'),
)
expect(balances_after[1]).toEqual(Value.fromEther('1'))
expect(balances_after[2]).toEqual(Value.fromEther('0.5'))
})

test('sender = EOA, executor = EOA', async () => {
const { authorization, eoa } = await setup()

const alice = privateKeyToAccount(Secp256k1.randomPrivateKey())
const bob = privateKeyToAccount(Secp256k1.randomPrivateKey())

const balances_before = await Promise.all([
getBalance(client, { address: eoa.address }),
getBalance(client, { address: alice.address }),
getBalance(client, { address: bob.address }),
])

expect(balances_before[0]).toEqual(Value.fromEther('2'))
expect(balances_before[1]).toEqual(Value.fromEther('0'))
expect(balances_before[2]).toEqual(Value.fromEther('0'))

await DelegatedAccount.execute(client, {
account: eoa,
authorizationList: [authorization],
calls: [
{ to: alice.address, value: Value.fromEther('1') },
{ to: bob.address, value: Value.fromEther('0.5') },
],
})

const balances_after = await Promise.all([
getBalance(client, { address: eoa.address }),
getBalance(client, { address: alice.address }),
getBalance(client, { address: bob.address }),
])

expect(balances_after[0]).not.toBeGreaterThan(
balances_before[0] - Value.fromEther('1'),
)
expect(balances_after[1]).toEqual(Value.fromEther('1'))
expect(balances_after[2]).toEqual(Value.fromEther('0.5'))
})
})
89 changes: 89 additions & 0 deletions src/core/internal/delegatedAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type * as Address from 'ox/Address'
import * as Hex from 'ox/Hex'

Check failure on line 2 in src/core/internal/delegatedAccount.ts

View workflow job for this annotation

GitHub Actions / Verify / Checks

'Hex' is declared but its value is never read.
import type { Account, Chain, Client, Transport } from 'viem'
import {
type ExecuteParameters,
type ExecuteReturnType,
execute as execute_viem,
} from 'viem/experimental/erc7821'

import { readContract } from 'viem/actions'

Check failure on line 10 in src/core/internal/delegatedAccount.ts

View workflow job for this annotation

GitHub Actions / Verify / Checks

'readContract' is declared but its value is never read.

import { delegationAbi } from './generated.js'

Check failure on line 12 in src/core/internal/delegatedAccount.ts

View workflow job for this annotation

GitHub Actions / Verify / Checks

'delegationAbi' is declared but its value is never read.
import type * as Key from './key.js'

/** A delegated account. */
export type DelegatedAccount = {
address: Address.Address
keys: readonly Key.Key[]
label?: string | undefined
}

/**
* Executes a set of calls on a delegated account.
*
* @param client - Client.
* @param parameters - Execution parameters.
* @returns Transaction hash.
*/
export async function execute<
const calls extends readonly unknown[],
chain extends Chain | undefined,
>(
client: Client<Transport, chain>,
parameters: execute.Parameters<calls, chain>,
): Promise<execute.ReturnType> {
const { account, executor, ...rest } = parameters
try {
return await execute_viem(client, {
...rest,
address: account.address,
account: typeof executor === 'undefined' ? account : executor,
} as ExecuteParameters)
} catch (error) {
// biome-ignore lint/complexity/noUselessCatch: TODO: Handle contract errors
throw error
}
}

export declare namespace execute {
export type Parameters<
calls extends readonly unknown[] = readonly unknown[],
chain extends Chain | undefined = Chain | undefined,
> = Omit<
ExecuteParameters<calls, chain>,
'account' | 'address' | 'opData'
> & {
/**
* The delegated account to execute the calls on.
*
* - `DelegatedAccount`: account that was instantiated with `Delegation.create` or `Delegation.from`.
* - `Account`: Viem account that has delegated to Porto.
*/
account: DelegatedAccount | Account
/**
* The executor of the execute transaction.
*
* - `Account`: execution will be attempted with the specified account.
* - `null`: the transaction will be filled by the JSON-RPC server.
* - `undefined`: execution will be attempted with the `account` value.
*/
executor?: Account | undefined | null
/**
* The index of the key to use on the delegated account to sign the execution.
*/
keyIndex?: number | undefined
}

export type ReturnType = ExecuteReturnType
}

/**
* Instantiates a delegated account.
*
* @param account - Account to instantiate.
* @returns An instantiated delegated account.
*/
export function from(account: DelegatedAccount): DelegatedAccount {
return account
}
Loading

0 comments on commit 5ea8d90

Please sign in to comment.