Skip to content

Recovery extension #735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf86ff6
Add recovery module on signUp and login
Agusx1211 Apr 23, 2025
402ee4d
Remvoe recovery on logout
Agusx1211 Apr 23, 2025
bc9e571
Move recovery to its own module
Agusx1211 Apr 23, 2025
4fbbcf0
Basic recovery methods
Agusx1211 Apr 24, 2025
aa3e118
Replace Janitor with generic Cron
Agusx1211 Apr 24, 2025
b88e594
Listen for recovery payloads
Agusx1211 Apr 24, 2025
601d5ec
Expose recovery methods
Agusx1211 Apr 24, 2025
f2871b1
Fix typo
Agusx1211 Apr 24, 2025
0b5fcac
Remove locks polyfill
Agusx1211 Apr 25, 2025
65fd7ab
Handle failed jobs
Agusx1211 Apr 25, 2025
00cf888
Initialize modules
Agusx1211 Apr 25, 2025
6bd2289
30000 days timelock is a bit long
Agusx1211 Apr 25, 2025
7a38a2b
Device as initialy recovery
Agusx1211 Apr 25, 2025
1eb343d
Set as logging-out when request has been created
Agusx1211 Apr 25, 2025
e1025dd
Recovery encoding fixes
Agusx1211 Apr 25, 2025
3a5eefe
Merge branch 'master' of github.com:0xsequence/sequence.js into recov…
Agusx1211 Apr 28, 2025
a62ae3c
Merge branch 'master' of github.com:0xsequence/sequence.js into recov…
Agusx1211 Apr 29, 2025
6809891
Generic requestConfigurationUpdate method
Agusx1211 Apr 30, 2025
5202f8d
Standalone remove and add recovery methods
Agusx1211 Apr 30, 2025
f0a5171
Complete remove recovery methods
Agusx1211 Apr 30, 2025
88936d3
Add payload to state tracker
Agusx1211 Apr 30, 2025
47f95cd
Save recovery payload
Agusx1211 May 2, 2025
dddc306
Mock locks for tests
Agusx1211 May 5, 2025
b26f78a
Fix recovery queued execution and test
Agusx1211 May 5, 2025
e1c5da7
Clear executed payloads
Agusx1211 May 5, 2025
05bfa95
Merge branch 'master' of github.com:0xsequence/sequence.js into recov…
Agusx1211 May 5, 2025
04ca548
Start Anvil for tests CLI
Agusx1211 May 5, 2025
35877ad
Check if module initialize is a function
Agusx1211 May 5, 2025
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
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/install-dependencies
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Start Anvil in background
run: anvil --fork-url https://nodes.sequence.app/arbitrum &
- run: pnpm test

# NOTE: if you'd like to see example of how to run
Expand Down
24 changes: 24 additions & 0 deletions packages/wallet/core/src/state/cached.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,28 @@ export class Cached implements Provider {
saveDeploy(imageHash: Hex.Hex, context: Context.Context): MaybePromise<void> {
return this.args.source.saveDeploy(imageHash, context)
}

async getPayload(opHash: Hex.Hex): Promise<
| {
chainId: bigint
payload: Payload.Parented
wallet: Address.Address
}
| undefined
> {
const cached = await this.args.cache.getPayload(opHash)
if (cached) {
return cached
}

const source = await this.args.source.getPayload(opHash)
if (source) {
await this.args.cache.savePayload(source.wallet, source.payload, source.chainId)
}
return source
}

savePayload(wallet: Address.Address, payload: Payload.Parented, chainId: bigint): MaybePromise<void> {
return this.args.source.savePayload(wallet, payload, chainId)
}
}
4 changes: 4 additions & 0 deletions packages/wallet/core/src/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export interface Reader {
): MaybePromise<Array<{ imageHash: Hex.Hex; signature: Signature.RawSignature }>>

getTree(rootHash: Hex.Hex): MaybePromise<GenericTree.Tree | undefined>
getPayload(
opHash: Hex.Hex,
): MaybePromise<{ chainId: bigint; payload: Payload.Parented; wallet: Address.Address } | undefined>
}

export interface Writer {
Expand All @@ -71,6 +74,7 @@ export interface Writer {

saveConfiguration(config: Config.Config): MaybePromise<void>
saveDeploy(imageHash: Hex.Hex, context: Context.Context): MaybePromise<void>
savePayload(wallet: Address.Address, payload: Payload.Parented, chainId: bigint): MaybePromise<void>
}

export type MaybePromise<T> = T | Promise<T>
Expand Down
12 changes: 12 additions & 0 deletions packages/wallet/core/src/state/local/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,18 @@ export class Provider implements ProviderInterface {
context,
)
}

async getPayload(
opHash: Hex.Hex,
): Promise<{ chainId: bigint; payload: Payload.Parented; wallet: Address.Address } | undefined> {
const data = await this.store.loadPayloadOfSubdigest(opHash)
return data ? { chainId: data.chainId, payload: data.content, wallet: data.wallet } : undefined
}

savePayload(wallet: Address.Address, payload: Payload.Parented, chainId: bigint): Promise<void> {
const subdigest = Hex.fromBytes(Payload.hash(wallet, chainId, payload))
return this.store.savePayloadOfSubdigest(subdigest, { content: payload, chainId, wallet })
}
}

export * from './memory.js'
Expand Down
22 changes: 22 additions & 0 deletions packages/wallet/core/src/state/remote/dev-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,26 @@ export class DevHttpProvider implements Provider {
saveDeploy(imageHash: Hex.Hex, context: Context.Context): Promise<void> {
return this.request<void>('POST', '/deploy', { imageHash, context })
}

async getPayload(opHash: Hex.Hex): Promise<
| {
chainId: bigint
payload: Payload.Parented
wallet: Address.Address
}
| undefined
> {
return this.request<
| {
chainId: bigint
payload: Payload.Parented
wallet: Address.Address
}
| undefined
>('GET', `/payload/${opHash}`)
}

async savePayload(wallet: Address.Address, payload: Payload.Parented, chainId: bigint): Promise<void> {
return this.request<void>('POST', '/payload', { wallet, payload, chainId })
}
}
29 changes: 18 additions & 11 deletions packages/wallet/core/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,23 +203,30 @@ export class Wallet {
}
}

async getNonce(provider: Provider.Provider, space: bigint): Promise<bigint> {
const result = await provider.request({
method: 'eth_call',
params: [{ to: this.address, data: AbiFunction.encodeData(Constants.READ_NONCE, [space]) }],
})

if (result === '0x' || result.length === 0) {
return 0n
}

return BigInt(result)
}

async prepareTransaction(
provider: Provider.Provider,
calls: Payload.Call[],
options?: { space?: bigint },
): Promise<Envelope.Envelope<Payload.Calls>> {
const space = options?.space ?? 0n
const status = await this.getStatus(provider)

let nonce: bigint = 0n
if (status.isDeployed) {
nonce = BigInt(
await provider.request({
method: 'eth_call',
params: [{ to: this.address, data: AbiFunction.encodeData(Constants.READ_NONCE, [space]) }],
}),
)
}
const [chainId, nonce] = await Promise.all([
provider.request({ method: 'eth_chainId' }),
this.getNonce(provider, space),
])

return {
payload: {
Expand All @@ -228,7 +235,7 @@ export class Wallet {
nonce,
calls,
},
...(await this.prepareBlankEnvelope(status.chainId ?? 0n)),
...(await this.prepareBlankEnvelope(BigInt(chainId))),
}
}

Expand Down
100 changes: 5 additions & 95 deletions packages/wallet/primitives-cli/src/subcommands/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,6 @@ import type { CommandModule } from 'yargs'
import { Payload } from '@0xsequence/wallet-primitives'
import { fromPosOrStdin } from '../utils.js'

export const KIND_TRANSACTIONS = 0x00
const KIND_MESSAGE = 0x01
const KIND_CONFIG_UPDATE = 0x02
const KIND_DIGEST = 0x03

const BEHAVIOR_IGNORE_ERROR = 0x00
const BEHAVIOR_REVERT_ON_ERROR = 0x01
const BEHAVIOR_ABORT_ON_ERROR = 0x02

const CallAbi = [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'value' },
Expand All @@ -38,98 +29,17 @@ export const DecodedAbi = [
{ type: 'address[]', name: 'parentWallets' },
]

export interface SolidityDecoded {
kind: number
noChainId: boolean
calls: SolidityCall[]
space: bigint
nonce: bigint
message: string
imageHash: string
digest: string
parentWallets: string[]
}

interface SolidityCall {
to: string
value: bigint
data: string
gasLimit: bigint
delegateCall: boolean
onlyFallback: boolean
behaviorOnError: bigint
}

function behaviorOnError(behavior: number): 'ignore' | 'revert' | 'abort' {
switch (behavior) {
case BEHAVIOR_IGNORE_ERROR:
return 'ignore'
case BEHAVIOR_REVERT_ON_ERROR:
return 'revert'
case BEHAVIOR_ABORT_ON_ERROR:
return 'abort'
default:
throw new Error(`Unknown behavior: ${behavior}`)
}
}

export async function doConvertToAbi(_payload: string): Promise<string> {
// Not implemented yet, but following the pattern
throw new Error('Not implemented')
}

export function solidityEncodedToParentedPayload(decoded: SolidityDecoded): Payload.Parented {
if (decoded.kind === KIND_TRANSACTIONS) {
return {
type: 'call',
nonce: decoded.nonce,
space: decoded.space,
calls: decoded.calls.map((call) => ({
to: Address.from(call.to),
value: call.value,
data: call.data as `0x${string}`,
gasLimit: call.gasLimit,
delegateCall: call.delegateCall,
onlyFallback: call.onlyFallback,
behaviorOnError: behaviorOnError(Number(call.behaviorOnError)),
})),
parentWallets: decoded.parentWallets.map((wallet) => Address.from(wallet)),
}
}

if (decoded.kind === KIND_MESSAGE) {
return {
type: 'message',
message: decoded.message as `0x${string}`,
parentWallets: decoded.parentWallets.map((wallet) => Address.from(wallet)),
}
}

if (decoded.kind === KIND_CONFIG_UPDATE) {
return {
type: 'config-update',
imageHash: decoded.imageHash as `0x${string}`,
parentWallets: decoded.parentWallets.map((wallet) => Address.from(wallet)),
}
}

if (decoded.kind === KIND_DIGEST) {
return {
type: 'digest',
digest: decoded.digest as `0x${string}`,
parentWallets: decoded.parentWallets.map((wallet) => Address.from(wallet)),
}
}

throw new Error('Not implemented')
}

export async function doConvertToPacked(payload: string, wallet?: string): Promise<string> {
const decodedPayload = solidityEncodedToParentedPayload(
const decodedPayload = Payload.fromAbiFormat(
AbiParameters.decode(
[{ type: 'tuple', name: 'payload', components: DecodedAbi }],
payload as Hex.Hex,
)[0] as unknown as SolidityDecoded,
)[0] as unknown as Payload.SolidityDecoded,
)

if (Payload.isCalls(decodedPayload)) {
Expand All @@ -144,7 +54,7 @@ export async function doConvertToJson(payload: string): Promise<string> {
const decoded = AbiParameters.decode(
[{ type: 'tuple', name: 'payload', components: DecodedAbi }],
payload as Hex.Hex,
)[0] as unknown as SolidityDecoded
)[0] as unknown as Payload.SolidityDecoded

const json = JSON.stringify(decoded)
return json
Expand All @@ -154,9 +64,9 @@ export async function doHash(wallet: string, chainId: bigint, payload: string):
const decoded = AbiParameters.decode(
[{ type: 'tuple', name: 'payload', components: DecodedAbi }],
payload as Hex.Hex,
)[0] as unknown as SolidityDecoded
)[0] as unknown as Payload.SolidityDecoded

return Hex.from(Payload.hash(Address.from(wallet), chainId, solidityEncodedToParentedPayload(decoded)))
return Hex.from(Payload.hash(Address.from(wallet), chainId, Payload.fromAbiFormat(decoded)))
}

const payloadCommand: CommandModule = {
Expand Down
4 changes: 2 additions & 2 deletions packages/wallet/primitives/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ export function findSignerLeaf(
}

export function getWeight(
topology: RawTopology | RawConfig,
topology: RawTopology | RawConfig | Config,
canSign: (signer: SignerLeaf | SapientSignerLeaf) => boolean,
): { weight: bigint; maxWeight: bigint } {
topology = isRawConfig(topology) ? topology.topology : topology
topology = isRawConfig(topology) || isConfig(topology) ? topology.topology : topology

if (isSignedSignerLeaf(topology)) {
return { weight: topology.weight, maxWeight: topology.weight }
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/primitives/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Address } from 'ox'

export type Extensions = {
passkeys: Address.Address
recovery: Address.Address
}

export const Dev1: Extensions = {
passkeys: '0x8f26281dB84C18aAeEa8a53F94c835393229d296',
recovery: '0xd98da48C4FF9c19742eA5856A277424557C863a6',
}

export * as Passkeys from './passkeys.js'
Expand Down
Loading