Skip to content
Draft
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 @@ -25,7 +25,7 @@
"turbo": "^2.5.4",
"typescript": "5.8.3"
},
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@10.18.3",
"engines": {
"node": ">=18"
}
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @0xsequence/wallet-migration

See [0xsequence project page](https://github.com/0xsequence/sequence.js).
48 changes: 48 additions & 0 deletions packages/utils/migration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@0xsequence/wallet-migration",
"version": "0.0.0",
"license": "Apache-2.0",
"type": "module",
"publishConfig": {
"access": "public"
},
"private": false,
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"devDependencies": {
"@0xsequence/relayerv2": "npm:@0xsequence/relayer@^2.3.29",
"@0xsequence/signhubv2": "npm:@0xsequence/signhub@^2.3.29",
"@repo/typescript-config": "workspace:^",
"@types/node": "^22.15.29",
"@vitest/coverage-v8": "^3.2.4",
"dotenv": "^16.5.0",
"ethers": "6.15.0",
"fake-indexeddb": "^6.0.1",
"typescript": "^5.8.3",
"vitest": "^3.2.1"
},
"dependencies": {
"@0xsequence/abi": "workspace:^",
"@0xsequence/v2core": "npm:@0xsequence/core@^2.3.29",
"@0xsequence/v2sessions": "npm:@0xsequence/sessions@^2.3.29",
"@0xsequence/v2migration": "npm:@0xsequence/migration@^2.3.29",
"@0xsequence/v2wallet": "npm:@0xsequence/wallet@^2.3.29",
"@0xsequence/wallet-core": "workspace:^",
"@0xsequence/wallet-primitives": "workspace:^",
"mipd": "^0.0.7",
"ox": "^0.7.2",
"viem": "^2.30.6"
}
}
2 changes: 2 additions & 0 deletions packages/utils/migration/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as migration from './migrations/index.js'
export * from './types.js'
79 changes: 79 additions & 0 deletions packages/utils/migration/src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { State } from '@0xsequence/wallet-core'
import { Payload } from '@0xsequence/wallet-primitives'
import { Address, Hex } from 'ox'
import { UnsignedMigration } from '../types.js'
import { MigrationEncoder_v1v3 } from './v1/encoder_v1_v3.js'

export interface MigrationEncoder<FromConfigType, ToConfigType, ToContextType, ConvertOptionsType, PrepareOptionsType> {
fromVersion: number
toVersion: number

/**
* Converts from `FromConfigType` to `ToConfigType`
* @param fromConfig The configuration to convert from
* @param options The convert options
* @returns The converted configuration
*/
convertConfig: (fromConfig: FromConfigType, options: ConvertOptionsType) => Promise<ToConfigType>

/**
* Prepares a migration for a given wallet address and context
* @param walletAddress The wallet address to prepare the migration for
* @param contexts The contexts to prepare the migration for
* @param toConfig The configuration to prepare the migration for
* @param options The prepare options
* @returns The migration payload to be signed
*/
prepareMigration: (
walletAddress: Address.Address,
toContext: ToContextType,
toConfig: ToConfigType,
options: PrepareOptionsType,
) => Promise<UnsignedMigration>

/**
* Encodes the a transaction for a given migration
* @param migration The migration to encode the transaction for
* @returns The encoded transaction
*/
toTransactionData: (migration: State.Migration) => Promise<{
to: Address.Address
data: Hex.Hex
}>

/**
* Decodes the payload from a migration
* @param payload The payload to decode
* @returns The decoded address and resulting image hash for the migration payload
*/
decodePayload: (payload: Payload.Calls) => Promise<{
address: Address.Address
toImageHash: Hex.Hex
}>
}

export interface Migrator<FromWallet, ToWallet, ConvertOptionsType> {
fromVersion: number
toVersion: number

convertWallet: (fromWallet: FromWallet, options: ConvertOptionsType) => Promise<ToWallet>
}

export const encoders: MigrationEncoder<any, any, any, any, any>[] = [new MigrationEncoder_v1v3()]

export function getMigrationEncoder<
FromConfigType,
ToConfigType,
ToContextType,
ConvertOptionsType,
PrepareOptionsType,
>(
fromVersion: number,
toVersion: number,
): MigrationEncoder<FromConfigType, ToConfigType, ToContextType, ConvertOptionsType, PrepareOptionsType> {
const encoder = encoders.find((encoder) => encoder.fromVersion === fromVersion && encoder.toVersion === toVersion)
if (!encoder) {
throw new Error(`Unsupported from version: ${fromVersion} to version: ${toVersion}`)
}
return encoder
}
63 changes: 63 additions & 0 deletions packages/utils/migration/src/migrations/v1/encoder_v1_v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { v1 } from '@0xsequence/v2core'
import { Config as V3Config, Context as V3Context } from '@0xsequence/wallet-primitives'
import { Address } from 'ox'
import { UnsignedMigration } from '../../types.js'
import { MigrationEncoder } from '../index.js'
import { BaseMigrationEncoder_v1v2, PrepareOptions as BasePrepareOptions } from '../v2/base.js'
import { ConvertOptions as V3ConvertOptions, createDefaultV3Topology } from '../v3/config.js'

export type ConvertOptions = V3ConvertOptions
export type PrepareOptions = BasePrepareOptions

// uint160(keccak256("org.sequence.sdk.migration.v1v3.space.nonce"))
export const MIGRATION_V1_V3_NONCE_SPACE = '0x9e4d5bdafd978baf1290aff23057245a2a62bef5'

export class MigrationEncoder_v1v3
extends BaseMigrationEncoder_v1v2
implements
MigrationEncoder<v1.config.WalletConfig, V3Config.Config, V3Context.Context, ConvertOptions, PrepareOptions>
{
fromVersion = 1
toVersion = 3

async convertConfig(fromConfig: v1.config.WalletConfig, options: ConvertOptions): Promise<V3Config.Config> {
if (fromConfig.version !== 1) {
throw new Error('Invalid v1 config')
}
const signerLeaves: V3Config.SignerLeaf[] = fromConfig.signers.map((signer) => ({
type: 'signer',
address: Address.from(signer.address),
weight: BigInt(signer.weight),
}))
const v1NestedTopology = V3Config.flatLeavesToTopology(signerLeaves)
return {
threshold: 1n,
checkpoint: 0n,
topology: [
{
type: 'nested',
weight: 1n,
threshold: BigInt(fromConfig.threshold),
tree: v1NestedTopology,
},
{
type: 'nested',
weight: 1n,
threshold: 2n,
tree: createDefaultV3Topology(options),
},
],
}
}

async prepareMigration(
walletAddress: Address.Address,
toContext: V3Context.Context,
toConfig: V3Config.Config,
options: PrepareOptions,
): Promise<UnsignedMigration> {
options.space = options.space ?? BigInt(MIGRATION_V1_V3_NONCE_SPACE)

return super.prepareMigrationToImplementation(walletAddress, toContext.stage2, toConfig, options)
}
}
89 changes: 89 additions & 0 deletions packages/utils/migration/src/migrations/v1/migrator_v1_v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { v1, commons as v2commons } from '@0xsequence/v2core'
import { WalletV1 } from '@0xsequence/v2wallet'
import { State, Wallet as WalletV3 } from '@0xsequence/wallet-core'
import { Payload, Context as V3Context } from '@0xsequence/wallet-primitives'
import { Address, Hex } from 'ox'
import { Migrator } from '../index.js'
import { ConvertOptions, MigrationEncoder_v1v3, PrepareOptions } from './encoder_v1_v3.js'

export type MigratorV1V3Options = ConvertOptions &
PrepareOptions & {
v3Context?: V3Context.Context
}

export class Migrator_v1v3 implements Migrator<WalletV1, WalletV3, MigratorV1V3Options> {
fromVersion = 1
toVersion = 3

constructor(
private readonly v3StateProvider: State.Provider,
private readonly encoder: MigrationEncoder_v1v3 = new MigrationEncoder_v1v3(),
) {}

private convertV1Context(v1Wallet: v2commons.context.WalletContext): V3Context.Context & { guest?: Address.Address } {
Hex.assert(v1Wallet.walletCreationCode)
return {
factory: Address.from(v1Wallet.factory),
stage1: Address.from(v1Wallet.mainModule),
stage2: Address.from(v1Wallet.mainModuleUpgradable),
creationCode: v1Wallet.walletCreationCode,
guest: Address.from(v1Wallet.guestModule),
}
}

async convertWallet(v1Wallet: WalletV1, options: MigratorV1V3Options): Promise<WalletV3> {
// Prepare configuration
const walletAddress = Address.from(v1Wallet.address)
const v3Context = options.v3Context || V3Context.Rc3
const v1Config = v1Wallet.config
const v3Config = await this.encoder.convertConfig(v1Config, options)

// Save v1 wallet information to v3 state provider
const v1ImageHash = v1.config.ConfigCoder.imageHashOf(v1Config)
Hex.assert(v1ImageHash)
if (this.v3StateProvider instanceof State.Sequence.Provider) {
// Force save the v1 configuration to key machine
const v1ServiceConfig = {
threshold: Number(v1Config.threshold),
signers: v1Config.signers.map(({ weight, address }) => ({ weight: Number(weight), address })),
}
await this.v3StateProvider.forceSaveConfiguration(v1ServiceConfig, this.fromVersion)
}
await this.v3StateProvider.saveDeploy(v1ImageHash, this.convertV1Context(v1Wallet.context))
await this.v3StateProvider.saveConfiguration(v3Config)

// Prepare migration
const unsignedMigration = await this.encoder.prepareMigration(walletAddress, v3Context, v3Config, options)

// Sign migration
const chainId = v1Wallet.chainId
const v2Nonce = v2commons.transaction.encodeNonce(unsignedMigration.payload.space, unsignedMigration.payload.nonce)
const txBundle: v2commons.transaction.TransactionBundle = {
entrypoint: walletAddress,
transactions: unsignedMigration.payload.calls.map((tx: Payload.Call) => ({
to: tx.to,
data: tx.data,
gasLimit: 0n,
revertOnError: true,
})),
nonce: v2Nonce,
}
const { signature } = await v1Wallet.signTransactionBundle(txBundle)
Hex.assert(signature)

// Save to tracker
const signedMigration: State.Migration = {
...unsignedMigration,
fromImageHash: v1ImageHash,
chainId: Number(chainId),
signature,
}
await this.v3StateProvider.saveMigration(walletAddress, signedMigration)

// Return v3 wallet
return new WalletV3(walletAddress, {
knownContexts: [{ name: 'v3', development: false, ...v3Context }],
stateProvider: this.v3StateProvider,
})
}
}
Loading
Loading