diff --git a/packages/sessions/src/tracker.ts b/packages/sessions/src/tracker.ts index 29c9dedb4..97e15d65e 100644 --- a/packages/sessions/src/tracker.ts +++ b/packages/sessions/src/tracker.ts @@ -18,7 +18,7 @@ export type ConfigDataDump = { presignedTransactions: PresignedConfigLink[] } -export abstract class ConfigTracker { +export interface ConfigTracker { loadPresignedConfiguration: (args: { wallet: string fromImageHash: string diff --git a/packages/sessions/src/trackers/arweave.ts b/packages/sessions/src/trackers/arweave.ts new file mode 100644 index 000000000..1ddebf091 --- /dev/null +++ b/packages/sessions/src/trackers/arweave.ts @@ -0,0 +1,626 @@ +import { commons, universal, v2 } from '@0xsequence/core' +import { migrator } from '@0xsequence/migration' +import { CachedEIP5719 } from '@0xsequence/replacer' +import { ethers } from 'ethers' +import { ConfigTracker, PresignedConfig, PresignedConfigLink } from '../tracker' + +const RATE_LIMIT_RETRY_DELAY = 5 * 60 * 1000 + +// depending on @0xsequence/abi breaks 0xsequence's proxy-transport-channel integration test +const MAIN_MODULE_ABI = [ + 'function execute((bool delegateCall, bool revertOnError, uint256 gasLimit, address target, uint256 value, bytes data)[] calldata transactions, uint256 nonce, bytes calldata signature)' +] + +export class ArweaveReader implements ConfigTracker, migrator.PresignedMigrationTracker { + private readonly configs: Map> = new Map() + + private readonly eip5719?: CachedEIP5719 + + constructor( + readonly namespace = 'Sequence-Sessions', + readonly owners?: string[], + eip5719Provider?: ethers.Provider + ) { + if (eip5719Provider) { + this.eip5719 = new CachedEIP5719(eip5719Provider) + } + } + + async loadPresignedConfiguration(args: { + wallet: string + fromImageHash: string + longestPath?: boolean + }): Promise { + const wallet = ethers.getAddress(args.wallet) + + const fromConfig = await this.configOfImageHash({ imageHash: args.fromImageHash }) + if (!fromConfig) { + throw new Error(`unable to find from config ${args.fromImageHash}`) + } + if (!v2.config.isWalletConfig(fromConfig)) { + throw new Error(`from config ${args.fromImageHash} is not v2`) + } + const fromCheckpoint = BigInt(fromConfig.checkpoint) + + const items = Object.entries( + await findItems({ Type: 'config update', Wallet: wallet }, { namespace: this.namespace, owners: this.owners }) + ).flatMap(([id, tags]) => { + try { + const { Signer: signer, Subdigest: subdigest, Digest: digest, 'To-Config': toImageHash } = tags + + let signatureType: 'eip-712' | 'eth_sign' | 'erc-1271' + switch (tags['Signature-Type']) { + case 'eip-712': + case 'eth_sign': + case 'erc-1271': + signatureType = tags['Signature-Type'] + break + default: + throw new Error(`unknown signature type ${tags['Signature-Type']}`) + } + + let toCheckpoint: bigint + try { + toCheckpoint = BigInt(tags['To-Checkpoint']) + } catch { + throw new Error(`to checkpoint is not a number: ${tags['To-Checkpoint']}`) + } + if (toCheckpoint <= fromCheckpoint) { + return [] + } + + if (!ethers.isAddress(signer)) { + throw new Error(`signer is not an address: ${signer}`) + } + + if (!ethers.isHexString(subdigest, 32)) { + throw new Error(`subdigest is not a hash: ${subdigest}`) + } + + if (!ethers.isHexString(digest, 32)) { + throw new Error(`digest is not a hash: ${digest}`) + } + + let chainId: bigint + try { + chainId = BigInt(tags['Chain-ID']) + } catch { + throw new Error(`chain id is not a number: ${tags['Chain-ID']}`) + } + + if (!ethers.isHexString(toImageHash, 32)) { + throw new Error(`to config is not a hash: ${toImageHash}`) + } + + return [{ id, signatureType, signer, subdigest, digest, chainId, toImageHash, toCheckpoint }] + } catch (error) { + console.warn(`invalid wallet ${wallet} config update ${id}:`, error) + return [] + } + }) + + const signatures: Map> = new Map() + let candidates: typeof items = [] + + for (const item of items) { + let imageHashSignatures = signatures.get(item.toImageHash) + if (!imageHashSignatures) { + imageHashSignatures = new Map() + signatures.set(item.toImageHash, imageHashSignatures) + candidates.push(item) + } + imageHashSignatures.set(item.signer, item) + } + + if (args.longestPath) { + candidates.sort(({ toCheckpoint: a }, { toCheckpoint: b }) => (a === b ? 0 : a < b ? -1 : 1)) + } else { + candidates.sort(({ toCheckpoint: a }, { toCheckpoint: b }) => (a === b ? 0 : a < b ? 1 : -1)) + } + + const updates: PresignedConfigLink[] = [] + + for (let currentConfig = fromConfig; candidates.length; ) { + const currentImageHash = v2.config.imageHash(currentConfig) + + let nextCandidate: (typeof candidates)[number] | undefined + let nextCandidateItems: Map + let nextCandidateSigners: string[] = [] + + for (const candidate of candidates) { + nextCandidateItems = signatures.get(candidate.toImageHash)! + nextCandidateSigners = Array.from(nextCandidateItems.keys()) + + const { weight } = v2.signature.encodeSigners( + currentConfig, + new Map(nextCandidateSigners.map(signer => [signer, { signature: '0x', isDynamic: false }])), + [], + 0 + ) + + if (weight >= BigInt(currentConfig.threshold)) { + nextCandidate = candidate + break + } + } + + if (!nextCandidate) { + console.warn(`unreachable configs with checkpoint > ${currentConfig.checkpoint} from config ${currentImageHash}`) + break + } + + const nextImageHash = nextCandidate.toImageHash + + try { + const nextConfig = await this.configOfImageHash({ imageHash: nextImageHash }) + if (!nextConfig) { + throw new Error(`unable to find config ${nextImageHash}`) + } + if (!v2.config.isWalletConfig(nextConfig)) { + throw new Error(`config ${nextImageHash} is not v2`) + } + + const nextCandidateSignatures = new Map( + ( + await Promise.all( + nextCandidateSigners.map(async signer => { + const { id, subdigest, signatureType } = nextCandidateItems.get(signer)! + try { + let signature = await (await fetchItem(id)).text() + switch (signatureType) { + case 'eip-712': + signature += '01' + break + case 'eth_sign': + signature += '02' + break + case 'erc-1271': + signature += '03' + break + } + if (this.eip5719) { + try { + signature = ethers.hexlify(await this.eip5719.runByEIP5719(signer, subdigest, signature)) + } catch (error) { + console.warn(`unable to run eip-5719 on config update ${id}`) + } + } + const recovered = commons.signer.tryRecoverSigner(subdigest, signature) + return [[signer, { signature, isDynamic: recovered !== signer }] as const] + } catch (error) { + console.warn(`unable to fetch signer ${signer} config update ${id}:`, error) + return [] + } + }) + ) + ).flat() + ) + + const { encoded: signature, weight } = v2.signature.encodeSigners(currentConfig, nextCandidateSignatures, [], 0) + if (weight < BigInt(currentConfig.threshold)) { + throw new Error(`insufficient signing power ${weight.toString()} < ${currentConfig.threshold}`) + } + updates.push({ wallet, signature, nextImageHash }) + + currentConfig = nextConfig + candidates = candidates.filter(({ toCheckpoint }) => toCheckpoint > BigInt(currentConfig.checkpoint)) + } catch (error) { + console.warn( + `unable to reconstruct wallet ${wallet} update from config ${currentImageHash} to config ${nextImageHash}:`, + error + ) + candidates = candidates.filter(({ toImageHash }) => toImageHash !== nextImageHash) + } + } + + return updates + } + + savePresignedConfiguration(_args: PresignedConfig): Promise { + throw new Error('arweave backend does not support saving config updates') + } + + saveWitnesses(_args: { wallet: string; digest: string; chainId: ethers.BigNumberish; signatures: string[] }): Promise { + throw new Error('arweave backend does not support saving signatures') + } + + async configOfImageHash(args: { imageHash: string; noCache?: boolean }): Promise { + if (!args.noCache) { + const config = this.configs.get(args.imageHash) + if (config) { + try { + return await config + } catch { + const config = this.configs.get(args.imageHash) + if (config) { + return config + } + } + } + } + + const config = (async (imageHash: string): Promise => { + const items = Object.entries( + await findItems({ Type: 'config', Config: imageHash }, { namespace: this.namespace, owners: this.owners }) + ).flatMap(([id, tags]) => { + try { + const version = Number(tags.Version) + if (!version) { + throw new Error(`invalid version: ${tags.Version}`) + } + + return [{ id, version }] + } catch (error) { + console.warn(`config ${imageHash} at ${id} invalid:`, error) + return [] + } + }) + + switch (items.length) { + case 0: + this.configs.set(imageHash, Promise.resolve(undefined)) + return + case 1: + break + default: + console.warn(`multiple configs ${imageHash} at ${items.map(({ id }) => id).join(', ')}`) + break + } + + for (const { id, version } of items) { + try { + const config = { ...(await (await fetchItem(id)).json()), version } + if (config.tree) { + config.tree = toTopology(config.tree) + } + + const actual = universal.coderFor(version).config.imageHashOf(config) + if (actual !== imageHash) { + throw new Error(`image hash is ${actual}, expected ${imageHash}`) + } + + this.configs.set(imageHash, Promise.resolve(config)) + return config + } catch (error) { + console.warn(`config at ${id} invalid:`, error) + } + } + + this.configs.set(imageHash, Promise.resolve(undefined)) + return + })(args.imageHash) + + if (!args.noCache) { + this.configs.set(args.imageHash, config) + } + + return config + } + + saveWalletConfig(_args: { config: commons.config.Config }): Promise { + throw new Error('arweave backend does not support saving configs') + } + + async imageHashOfCounterfactualWallet(args: { + wallet: string + noCache?: boolean + }): Promise<{ imageHash: string; context: commons.context.WalletContext } | undefined> { + const wallet = ethers.getAddress(args.wallet) + + const items = Object.entries( + await findItems({ Type: 'wallet', Wallet: wallet }, { namespace: this.namespace, owners: this.owners }) + ).flatMap(([id, tags]) => { + try { + const { 'Deploy-Config': imageHash } = tags + + const version = Number(tags['Deploy-Version']) + if (!version) { + throw new Error(`invalid version: ${tags['Deploy-Version']}`) + } + + if (!imageHash) { + throw new Error('no deploy config') + } + + const context = commons.context.defaultContexts[version] + if (!context) { + throw new Error(`unknown version: ${version}`) + } + + if (commons.context.addressOf(context, imageHash) !== wallet) { + throw new Error(`incorrect v${version} deploy config: ${imageHash}`) + } + + return [{ id, imageHash, context }] + } catch (error) { + console.warn(`wallet ${wallet} at ${id} invalid:`, error) + return [] + } + }) + + switch (items.length) { + case 0: + return + case 1: + break + default: + console.warn(`multiple deploy configs for wallet ${wallet} at ${items.map(({ id }) => id).join(', ')}, using first`) + break + } + + return items[0] + } + + saveCounterfactualWallet(_args: { config: commons.config.Config; context: commons.context.WalletContext[] }): Promise { + throw new Error('arweave backend does not support saving wallets') + } + + async walletsOfSigner(args: { + signer: string + noCache?: boolean + allSignatures?: boolean + }): Promise> { + const signer = ethers.getAddress(args.signer) + + const proofs: Map }> = new Map() + + for (const [id, tags] of Object.entries( + await findItems( + { Type: ['signature', 'config update'], Signer: signer, Witness: args.allSignatures ? undefined : 'true' }, + { namespace: this.namespace, owners: this.owners } + ) + )) { + const { Wallet: wallet, Subdigest: subdigest, Digest: digest, 'Chain-ID': chainId } = tags + + try { + if (proofs.has(wallet)) { + continue + } + + let signatureType: '01' | '02' | '03' + switch (tags['Signature-Type']) { + case 'eip-712': + signatureType = '01' + break + case 'eth_sign': + signatureType = '02' + break + case 'erc-1271': + signatureType = '03' + break + default: + throw new Error(`unknown signature type ${tags['Signature-Type']}`) + } + + if (subdigest !== commons.signature.subdigestOf({ digest, chainId, address: wallet })) { + throw new Error('incorrect subdigest') + } + + const signature = fetchItem(id).then(async response => { + const signature = (await response.text()) + signatureType + if (this.eip5719) { + try { + return ethers.hexlify(await this.eip5719.runByEIP5719(signer, subdigest, signature)) + } catch (error) { + console.warn(`unable to run eip-5719 on signature ${id}`) + } + } + return signature + }) + + proofs.set(wallet, { digest, chainId: BigInt(chainId), signature }) + } catch (error) { + console.warn(`signer ${signer} signature ${id} of wallet ${wallet} invalid:`, error) + } + } + + return Promise.all( + [...proofs.entries()].map(async ([wallet, { digest, chainId, signature }]) => ({ + wallet, + proof: { digest, chainId, signature: await signature } + })) + ) + } + + async getMigration( + address: string, + fromImageHash: string, + fromVersion: number, + chainId: ethers.BigNumberish + ): Promise { + const wallet = ethers.getAddress(address) + + const items = Object.entries( + await findItems( + { + Type: 'migration', + Migration: wallet, + 'Chain-ID': BigInt(chainId).toString(), + 'From-Version': `${fromVersion}`, + 'From-Config': fromImageHash + }, + { namespace: this.namespace, owners: this.owners } + ) + ).flatMap(([id, tags]) => { + try { + const { 'To-Config': toImageHash, Executor: executor } = tags + + const toVersion = Number(tags['To-Version']) + if (!toVersion) { + throw new Error(`invalid version: ${tags['To-Version']}`) + } + + if (!ethers.isHexString(toImageHash, 32)) { + throw new Error(`to config is not a hash: ${toImageHash}`) + } + + if (!ethers.isAddress(executor)) { + throw new Error(`executor is not an address: ${executor}`) + } + + return { id, toVersion, toImageHash, executor } + } catch (error) { + console.warn( + `chain ${chainId} migration ${id} for v${fromVersion} wallet ${wallet} from config ${fromImageHash} invalid:`, + error + ) + return [] + } + }) + + switch (items.length) { + case 0: + return + case 1: + break + default: + console.warn( + `multiple chain ${chainId} migrations for v${fromVersion} wallet ${wallet} from config ${fromImageHash} at ${items.map(({ id }) => id).join(', ')}, using first` + ) + break + } + + const { id, toVersion, toImageHash, executor } = items[0] + + const [data, toConfig] = await Promise.all([ + fetchItem(id).then(response => response.text()), + this.configOfImageHash({ imageHash: toImageHash }) + ]) + + if (!toConfig) { + throw new Error(`unable to find to config ${toImageHash} for migration`) + } + + const mainModule = new ethers.Interface(MAIN_MODULE_ABI) + const [encoded, nonce, signature] = mainModule.decodeFunctionData('execute', data) + const transactions = commons.transaction.fromTxAbiEncode(encoded) + const subdigest = commons.transaction.subdigestOfTransactions(wallet, chainId, nonce, transactions) + + return { + tx: { entrypoint: executor, transactions, nonce, chainId, intent: { id: subdigest, wallet }, signature }, + fromVersion, + toVersion: Number(toVersion), + toConfig + } + } + + saveMigration(_address: string, _signed: migrator.SignedMigration, _contexts: commons.context.VersionedContext): Promise { + throw new Error('arweave backend does not support saving migrations') + } +} + +async function findItems( + filter: { [name: string]: undefined | string | string[] }, + options?: { namespace?: string; owners?: string[]; pageSize?: number; maxResults?: number } +): Promise<{ [id: string]: { [tag: string]: string } }> { + const namespace = options?.namespace + const owners = options?.owners + const pageSize = options?.pageSize ?? 100 + const maxResults = options?.maxResults + + const tags = Object.entries(filter).flatMap(([name, values]) => + values === undefined + ? [] + : [ + `{ name: "${namespace ? `${namespace}-${name}` : name}", values: [${typeof values === 'string' ? `"${values}"` : values.map(value => `"${value}"`).join(', ')}] }` + ] + ) + + const edges: Array<{ cursor: string; node: { id: string; tags: Array<{ name: string; value: string }> } }> = [] + + for (let hasNextPage = true; hasNextPage && (maxResults === undefined || edges.length < maxResults); ) { + const query = ` + query { + transactions(sort: HEIGHT_DESC, ${edges.length ? `first: ${pageSize}, after: "${edges[edges.length - 1].cursor}"` : `first: ${pageSize}`}, tags: [${tags.join(', ')}]${owners === undefined ? '' : `, owners: [${owners.map(owner => `"${owner}"`).join(', ')}]`}) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + tags { + name + value + } + } + } + } + } + ` + + let response: Response + while (true) { + response = await fetch('https://arweave.net/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + redirect: 'follow' + }) + if (response.status !== 429) { + break + } + console.warn( + `rate limited by arweave.net, trying again in ${RATE_LIMIT_RETRY_DELAY / 1000} seconds at ${new Date(Date.now() + RATE_LIMIT_RETRY_DELAY).toLocaleTimeString()}` + ) + await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY)) + } + + const { + data: { transactions } + } = await response.json() + + edges.push(...transactions.edges) + + hasNextPage = transactions.pageInfo.hasNextPage + } + + return Object.fromEntries( + edges.map(({ node: { id, tags } }) => [ + id, + Object.fromEntries( + tags.map(({ name, value }) => [ + namespace && name.startsWith(`${namespace}-`) ? name.slice(namespace.length + 1) : name, + value + ]) + ) + ]) + ) +} + +async function fetchItem(id: string): Promise { + while (true) { + const response = await fetch(`https://arweave.net/${id}`, { redirect: 'follow' }) + if (response.status !== 429) { + return response + } + console.warn( + `rate limited by arweave.net, trying again in ${RATE_LIMIT_RETRY_DELAY / 1000} seconds at ${new Date(Date.now() + RATE_LIMIT_RETRY_DELAY).toLocaleTimeString()}` + ) + await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY)) + } +} + +function toTopology(topology: any): v2.config.Topology { + if (typeof topology === 'string') { + return { nodeHash: topology } + } + + if (typeof topology === 'object' && topology?.node !== undefined) { + return { nodeHash: topology.node } + } + + if (topology instanceof Array && topology.length === 2) { + return { left: toTopology(topology[0]), right: toTopology(topology[1]) } + } + + if (v2.config.isNode(topology)) { + return { left: toTopology(topology.left), right: toTopology(topology.right) } + } + + if (v2.config.isNestedLeaf(topology)) { + return { ...topology, tree: toTopology(topology.tree) } + } + + return topology +} diff --git a/packages/sessions/src/trackers/index.ts b/packages/sessions/src/trackers/index.ts index 05dddeb00..a26c0c789 100644 --- a/packages/sessions/src/trackers/index.ts +++ b/packages/sessions/src/trackers/index.ts @@ -1,3 +1,4 @@ +export * as arweave from './arweave' export * as debug from './debug' export * as local from './local' export * as remote from './remote' diff --git a/packages/sessions/tests/arweave.spec.ts b/packages/sessions/tests/arweave.spec.ts new file mode 100644 index 000000000..b35891c3b --- /dev/null +++ b/packages/sessions/tests/arweave.spec.ts @@ -0,0 +1,434 @@ +import { commons, universal, v2 } from '@0xsequence/core' +import { expect } from 'chai' +import { ethers } from 'ethers' + +import { trackers } from '../src' + +class MockProvider extends ethers.AbstractProvider { + _detectNetwork(): Promise { + return Promise.resolve(new ethers.Network('', 0)) + } + + _perform(_req: ethers.PerformActionRequest): Promise { + return Promise.resolve('0x1626ba7e00000000000000000000000000000000000000000000000000000000' as any) + } +} + +describe.only('Arweave config reader', () => { + const namespace = 'xOovxYFkIwBpEwSi' + const owners = ['lJYCA4xBPJeZSgr9AF_4pHp4HVGvTOa4NYKJRoMBP5c'] + const arweave = new trackers.arweave.ArweaveReader(namespace, owners) + const sessions = new trackers.remote.RemoteConfigTracker('http://localhost:5555') + const provider = new MockProvider() + + it('Should find the config for an image hash', async () => { + const imageHash = '0x8073858470016c4fdee9d3ad7c929e81cb19668a73fde061f00645228676e8dd' + + const config = await arweave.configOfImageHash({ imageHash }) + if (!config) { + throw new Error('config not found') + } + + const coder = universal.genericCoderFor(config.version) + expect(coder.config.imageHashOf(config)).to.equal(imageHash) + }) + + it('Should find the deploy hash for a wallet', async () => { + const address = '0x801DC9A5F00f781cA0f1ca56dbA68DA69fB07cdC' + + const wallet = await arweave.imageHashOfCounterfactualWallet({ wallet: address }) + if (!wallet) { + throw new Error('wallet not found') + } + + expect(commons.context.addressOf(wallet.context, wallet.imageHash)).to.equal(address) + }) + + it('Should find the wallets for a signer', async () => { + const signer = '0x8151D1B52dEb93eF2300884fC4CcddDDFf8C6BdA' + + const wallets = await arweave.walletsOfSigner({ signer }) + + expect(wallets.some(({ wallet }) => wallet === '0x213400e26b4aA36885Bcb29A8B7D67caeB0348EC')).to.be.true + + expect( + wallets.every( + ({ wallet, proof: { digest, chainId, signature } }) => + commons.signer.recoverSigner(commons.signature.subdigestOf({ digest, chainId, address: wallet }), signature) === signer + ) + ).to.be.true + }) + + it('Should find the shortest sequence of config updates from a config', async () => { + const wallet = '0x36f8D1327F738608e275226A6De2D1720AF5C896' + const fromImageHash = '0xacbf7d62011d908d4cdfc96651be39b77d56f8e8048e993a57470724eb6049be' + + const updates = await arweave.loadPresignedConfiguration({ wallet, fromImageHash }) + + expect(updates.every(update => update.wallet === wallet)).to.be.true + + expect(updates.map(({ nextImageHash }) => nextImageHash)).to.deep.equal([ + '0x08b597e0fc694da132d38db2b9e6c598ea85d786aba1c9830def5df0fec6da67', + '0xea570311d302ef75c4efe9da8041011a43b398682b5461bc3edfd5632fe36199' + ]) + + let imageHash = fromImageHash + + for (const { nextImageHash, signature } of updates) { + const digest = v2.chained.hashSetImageHash(nextImageHash) + const decoded = v2.signature.decodeSignature(signature) + const recovered = await v2.signature.recoverSignature(decoded, { digest, chainId: 0, address: wallet }, provider) + expect(v2.config.imageHash(recovered.config)).to.equal(imageHash) + imageHash = nextImageHash + } + }) + + it('Should find the longest sequence of config updates from a config', async () => { + const wallet = '0x36f8D1327F738608e275226A6De2D1720AF5C896' + const fromImageHash = '0xacbf7d62011d908d4cdfc96651be39b77d56f8e8048e993a57470724eb6049be' + + const updates = await arweave.loadPresignedConfiguration({ wallet, fromImageHash, longestPath: true }) + + expect(updates.every(update => update.wallet === wallet)).to.be.true + + expect(updates.map(({ nextImageHash }) => nextImageHash)).to.deep.equal([ + '0x8230d5841133b06eeeba92494fcf28d4c7ca50ae59f092d630dbee0d07c5e4f5', + '0x08b597e0fc694da132d38db2b9e6c598ea85d786aba1c9830def5df0fec6da67', + '0xea570311d302ef75c4efe9da8041011a43b398682b5461bc3edfd5632fe36199' + ]) + + let imageHash = fromImageHash + + for (const { nextImageHash, signature } of updates) { + const digest = v2.chained.hashSetImageHash(nextImageHash) + const decoded = v2.signature.decodeSignature(signature) + const recovered = await v2.signature.recoverSignature(decoded, { digest, chainId: 0, address: wallet }, provider) + expect(v2.config.imageHash(recovered.config)).to.equal(imageHash) + imageHash = nextImageHash + } + }) + + it('Should find a migration', async () => { + const address = '0x9efB45F2e6Bd007Bb47D94CcB461d0b88a1fc6d6' + const fromVersion = 1 + const fromImageHash = '0xb0c9bf9b74e670cd5245ac196261e16c092b701ea769269aeb0b1507bb96f961' + const toVersion = 2 + const toImageHash = '0xc289ea81fb71c62b4eb247c2e83b6897e1274e2ecd09d0cb780619cf4a4f204a' + const chainId = 1 + + const migration = await arweave.getMigration(address, fromImageHash, fromVersion, chainId) + if (!migration) { + throw new Error('migration not found') + } + + expect(migration.tx.intent.wallet).to.equal(address) + expect(BigInt(migration.tx.chainId)).to.equal(BigInt(chainId)) + expect(migration.fromVersion).to.equal(fromVersion) + expect(migration.toVersion).to.equal(toVersion) + expect(migration.toConfig.version).to.equal(toVersion) + + const toCoder = universal.genericCoderFor(migration.toVersion) + expect(toCoder.config.imageHashOf(migration.toConfig)).to.equal(toImageHash) + + const fromCoder = universal.genericCoderFor(migration.fromVersion) + const decoded = fromCoder.signature.decode(migration.tx.signature) + const digest = commons.transaction.digestOfTransactions(migration.tx.nonce, migration.tx.transactions) + const recovered = await fromCoder.signature.recover(decoded, { digest, chainId, address }, provider) + expect(fromCoder.config.imageHashOf(recovered.config)).to.equal(fromImageHash) + }) + + it.skip('Should find the same configs as Sequence Sessions', async () => { + const imageHashes = [ + '0x002f295ccfaf604ff09f200ad3282710f8b156811a03065dba66cf0902fff629', + '0x015cadeea08b6f9ed3181b1560a1f801e58c02f4bb0d33d01b0c1ab7b07b7bb1', + '0x042e86a1fe7f541e287b9053c483e21d059380b56d4baaa4161b0302cc55f22e', + '0x08b597e0fc694da132d38db2b9e6c598ea85d786aba1c9830def5df0fec6da67', + '0x08f915b8325e25003cc47e16fe144103655cda5e1cf43030345640c293feca98', + '0x105823726957bbef0932076d4209de8e0198fd5da59042221583c6ba26ef2637', + '0x1a13a8a34d18946b00b9368cf02f1cba3219eff2a18e76e4951d678824b34bdb', + '0x1d018d98154312f501d1794ed77abd2e544a0a7c035638e965be846c0c347f37', + '0x24f30c67a1f6228c2aa380e77999269a6203eab9ef60f3785ccd7a15ce199827', + '0x2513cf57aca274bc40c2bd6492a1499e8b781d128e39b8dd85659b75687beb47', + '0xeb718fb634b9f5e3a722825a25d4b48d3bbfe106d04efcbba6504fbf4539beed', + '0xecc51be6a1c52bb41183df18c5df0185e20014ffafa04096d9da1148525e476e', + '0xedfd70427e797f3865228c24d53903b0b529c544bf788000653070205e9548f2', + '0xef028a928c04ec9759be984c2f0f72e0aa578efc2f402dbb6ca4893e981bbf41', + '0xf148aa32b0cbd54c95610c8fb423b0506dd642ff659418a9ef64cfa50ef97489', + '0xf2e32da98766f93d86284e029565d814954163c15d681013e53b11b66e13bb0f', + '0xf4e8f9efa633938f6fbc02088074a9ee466178d59ff7ed8eb579ed7f14583dc5', + '0xf54e5829545147e687b7fe39e078de34dbd60dd24096a2deea1bb8dd86d93936', + '0xf5c2d2e6666cd2f04962df167eeeee5d217f731787a7d698b57142bb0da131d3', + '0xff9a2779f55740f1f4011a6a00fee48e717cd51b75e32dd6a7db97e33a7b3d07' + ] + + for (const imageHash of imageHashes) { + const [arweaveConfig, sessionsConfig] = await Promise.all([ + arweave.configOfImageHash({ imageHash }), + sessions.configOfImageHash({ imageHash }) + ]) + + expect(arweaveConfig).to.deep.equal(sessionsConfig) + } + }) + + it.skip('Should find the same deploy hashes as Sequence Sessions', async () => { + const wallets = [ + '0x1982D04a8473d391d4D4cA0312420F13Bb8dE26e', + '0x1dc2DA033d412d5E0D92f97a3157177dF41381D6', + '0x213400e26b4aA36885Bcb29A8B7D67caeB0348EC', + '0x329bD174c721bFa3A664Bde815dB33A7AA8b14a8', + '0x36f8D1327F738608e275226A6De2D1720AF5C896', + '0x41a0D39EFbB9a642c589abf2D501757D2f403470', + '0x55Cfd699C5E105180473c5A0403a3b491c82fb22', + '0x59756e67CFab5e1dFa1c7bCf7f5B04AbCAeb0B0F', + '0x6B7CE863DfcCeAbc6bDbC3f73f6def0de81dfe27', + '0x6Ce4229189988358073de1cd8AE7edFDf979635a', + '0xCa9A0F02c0589D550f06F78AfF604A5405b90448', + '0xD38A7FB85e76681cB409705622e877D11C7Cfe54', + '0xd5b1C31f7626A8Bd206D744128dFE6401dd7D7F6', + '0xDf29fF6EE710c4042dfE71bEeC1971Fca1F6A6F5', + '0xEf87203423cA064A44CE2661Daf93051e2F423a2', + '0xf3Da03EbBda88D28981c63Bd5ddA38d3eCff400a', + '0xf563fbB21208C7c915ea7d28014D36B1F9acACa9', + '0xFAE677fc10bDb6bF3C69Bb9DEEc6752cC7e06224', + '0xfBF80a987857e6dcAF790B297fC9a4f97DbbfBB0', + '0xfc9Adc5cd71F77e46a956F94df0fd5b0dF6Eef12' + ] + + for (const wallet of wallets) { + const [arweaveWallet, sessionsWallet] = await Promise.all([ + arweave.imageHashOfCounterfactualWallet({ wallet }), + sessions.imageHashOfCounterfactualWallet({ wallet }) + ]) + + expect(arweaveWallet?.imageHash).to.equal(sessionsWallet?.imageHash) + expect(arweaveWallet?.context).to.deep.equal(sessionsWallet?.context) + } + }) + + it.skip('Should find the same wallets as Sequence Sessions', async () => { + const signers = [ + '0x079cc5A64Fa4Bdd928bbF0EaBaf7BE91D633abf5', + '0x18510092ee248b1A2BBaB66C5d223EBa784693BA', + '0x1BA6a414d3C45a8E21FBEf451882170d0f6807F7', + '0x1Cd69D558cbD121F6C4DdF11db2CaCC142705a20', + '0x24270586957918c5C075E970A208387C888C4dD8', + '0x289cF67aeF2000DEcafb525103d8aDE044996D45', + '0x37Fd684c78b74b633CA43Ca5418f6f80827fB0fD', + '0x5373B3264EbbF0471FE4CC8B63f30446Cc03F6ad', + '0x553390e8B3dd2694Ea50bE9972C0D66b320bBa27', + '0x58AF1d8567BE0629A9961d8B3e06234B0f731187', + '0xb478671F3705cC2a3A1F47326F2Ef93853b79cf2', + '0xbb8FAEc13852b263644e75fd568B422055A8e8DC', + '0xbcB1EFB67f277cBbBeB886D6248ab222f3ef2195', + '0xc37c114B99242D1F83fFD964236f45042eD8c162', + '0xCa968ebc798feaeE466e73C872f085C3A2c9b7D9', + '0xcD2C0E8b8372FfF16caa0a29F9336F4dFB4D2EA1', + '0xd4c04c7392617D85b6FF33E203714C6Fd46336b4', + '0xe7D97e2d43900297a7537B0eD3B6C27306f6aDC0', + '0xea5dE55520f4cca364AB9Ed5613a11aa1e5C977E', + '0xFf6bEB351a06f35BFD6074d6Cfe34fcb8734F675' + ] + + for (const signer of signers) { + const [arweaveWallets, sessionsWallets] = await Promise.all([ + arweave.walletsOfSigner({ signer }), + sessions.walletsOfSigner({ signer }) + ]) + + expect(Object.fromEntries(arweaveWallets.map(({ wallet, proof }) => [wallet, proof]))).to.deep.equal( + Object.fromEntries(sessionsWallets.map(({ wallet, proof }) => [wallet, proof])) + ) + } + }) + + it.skip('Should find the same config updates as Sequence Sessions', async () => { + const updates = [ + { + wallet: '0x1982D04a8473d391d4D4cA0312420F13Bb8dE26e', + fromImageHash: '0x8073858470016c4fdee9d3ad7c929e81cb19668a73fde061f00645228676e8dd' + }, + { + wallet: '0x213400e26b4aA36885Bcb29A8B7D67caeB0348EC', + fromImageHash: '0x653ad79e81e77bbf9aeca4740a12dbe260e17abde4114c4a4056d7c8ab605270' + }, + { + wallet: '0x329bD174c721bFa3A664Bde815dB33A7AA8b14a8', + fromImageHash: '0x47eb2d6796c08e627d886ce5dd88f4aefbda5ab6209a5e35ded2f5ea95a5f05a' + }, + { + wallet: '0x36f8D1327F738608e275226A6De2D1720AF5C896', + fromImageHash: '0xacbf7d62011d908d4cdfc96651be39b77d56f8e8048e993a57470724eb6049be' + }, + { + wallet: '0x41a0D39EFbB9a642c589abf2D501757D2f403470', + fromImageHash: '0x9cd23aa8bf0945ec412aa2c815ffbb77341a869a0c3d031af0bb0b82faa1fc75' + }, + { + wallet: '0x55Cfd699C5E105180473c5A0403a3b491c82fb22', + fromImageHash: '0xf54e5829545147e687b7fe39e078de34dbd60dd24096a2deea1bb8dd86d93936' + }, + { + wallet: '0x59756e67CFab5e1dFa1c7bCf7f5B04AbCAeb0B0F', + fromImageHash: '0x2a0b27a28d39ec7b4ad61edc83b55d9b8375252ad48c838252d937a7f4afcf89' + }, + { + wallet: '0x6B7CE863DfcCeAbc6bDbC3f73f6def0de81dfe27', + fromImageHash: '0x08f915b8325e25003cc47e16fe144103655cda5e1cf43030345640c293feca98' + }, + { + wallet: '0x6Ce4229189988358073de1cd8AE7edFDf979635a', + fromImageHash: '0xf4e8f9efa633938f6fbc02088074a9ee466178d59ff7ed8eb579ed7f14583dc5' + }, + { + wallet: '0x801DC9A5F00f781cA0f1ca56dbA68DA69fB07cdC', + fromImageHash: '0xab5e99dc4fc094955f547bce2b8e0991845aa17f4fab47e3d212131474982fd6' + }, + { + wallet: '0x82B772b0fDb7Efb31B7DDD8d06C7C10fa1Dca383', + fromImageHash: '0x99da13df61af5b72011ab2e81aea9c4960c58344f7e536a5db27ce887acf0799' + }, + { + wallet: '0x84ac87bc06De4e1456B9df2C2496bF9a12b86C10', + fromImageHash: '0xc36416d54ec63920066c441686788888ee5505cd9137a006e14419940d53222d' + }, + { + wallet: '0x8af66F10b45AE8eba55C819a702344c407fD97fE', + fromImageHash: '0x890364a08ba76febfc67d63507a362c00c71cf4cf67b88e68f6952a9b8b95c66' + }, + { + wallet: '0x8e17D9C9dF4271C9a3cb0D7635004257f9805A6F', + fromImageHash: '0x4aade79c43aa094d77d98f5e2f70efb28cc4670614ff5894713c3bb11d32d9cf' + }, + { + wallet: '0x93fe4617B114F4018eaCfBB7eAb00A06f8C54E2D', + fromImageHash: '0xe9ab45294e8e22a456ff493201bd6f3329a6875193a2b1afc2e357c813ce0842' + }, + { + wallet: '0x9876DD582d28a527586fee03311B4a57461fE4c7', + fromImageHash: '0x7bf4d1c4443f505e86495c4b1666e9484b9636ec53ef166695a6caf3ed03b3d6' + }, + { + wallet: '0x9A203aBD53719C04ad7E1A5e587ea636368A6ed1', + fromImageHash: '0xef028a928c04ec9759be984c2f0f72e0aa578efc2f402dbb6ca4893e981bbf41' + }, + { + wallet: '0x9BdD9F17370d5690230Ba6CdfCE6D40c0dE7Fb49', + fromImageHash: '0xd0fdc647d1fc584cb53bb2798abfd887e61aab0b038caa201b96bebd39e7565f' + }, + { + wallet: '0x9efB45F2e6Bd007Bb47D94CcB461d0b88a1fc6d6', + fromImageHash: '0x2693f1f40c73d0c1f361f472cd1ec4fac9daa2d5232ff5f5b87ec56c1d3e7e20' + }, + { + wallet: '0xa53f6C371539F53Bb4DbcA0f1351eA7AA7F488c5', + fromImageHash: '0x1d018d98154312f501d1794ed77abd2e544a0a7c035638e965be846c0c347f37' + } + ] + + for (const longestPath of [false, true]) { + for (const update of updates) { + const [arweaveUpdates, sessionsUpdates] = await Promise.all([ + arweave.loadPresignedConfiguration({ ...update, longestPath }), + sessions.loadPresignedConfiguration({ ...update, longestPath }) + ]) + + let imageHash = update.fromImageHash + + for (const i in arweaveUpdates) { + const arweaveUpdate = arweaveUpdates[i] + const sessionsUpdate = sessionsUpdates[i] + + expect(arweaveUpdate.wallet).to.equal(update.wallet) + expect(sessionsUpdate.wallet).to.equal(update.wallet) + expect(arweaveUpdate.nextImageHash).to.equal(sessionsUpdate.nextImageHash) + + const nextImageHash = arweaveUpdate.nextImageHash + + const arweaveSignature = v2.signature.decodeSignature(arweaveUpdate.signature) + const sessionsSignature = v2.signature.decodeSignature(sessionsUpdate.signature) + + const digest = v2.chained.hashSetImageHash(nextImageHash) + + const { config: arweaveConfig } = await v2.signature.recoverSignature( + arweaveSignature, + { digest, chainId: 0, address: update.wallet }, + provider + ) + + const { config: sessionsConfig } = await v2.signature.recoverSignature( + sessionsSignature, + { digest, chainId: 0, address: update.wallet }, + provider + ) + + expect(v2.config.imageHash(arweaveConfig)).to.equal(v2.config.imageHash(sessionsConfig)) + + imageHash = nextImageHash + } + } + } + }) + + it.skip('Should find the same migrations as Sequence Sessions', async () => { + const migrations = [ + { + address: '0x1dc2DA033d412d5E0D92f97a3157177dF41381D6', + fromVersion: 1, + fromImageHash: '0xd0cca2788f80d85e93a0b3dd2af2e5962979d162931ec9c4537318be0c8ca312', + chainId: 1 + }, + { + address: '0x213400e26b4aA36885Bcb29A8B7D67caeB0348EC', + fromVersion: 1, + fromImageHash: '0xd94d8b1eaeaa3e2053b3421898e7925ebeef760881d9866c0096a3f97ed78f59', + chainId: 1 + }, + { + address: '0x9efB45F2e6Bd007Bb47D94CcB461d0b88a1fc6d6', + fromVersion: 1, + fromImageHash: '0xb0c9bf9b74e670cd5245ac196261e16c092b701ea769269aeb0b1507bb96f961', + chainId: 1 + }, + { + address: '0xb0E931FB27cc7149Ce0B8585739414Bf0866E0d2', + fromVersion: 1, + fromImageHash: '0x784e8115d0da9724aabe8ce4b6c27a2750ca3bc0ce51f4404c0aee8a2856859d', + chainId: 1 + }, + { + address: '0xc3527Da8b07E49CA6cCCC773C8D032bd4a77D464', + fromVersion: 1, + fromImageHash: '0xa88c665c0507894572288103cb88eea73e791f686b9eb2a4c80b1ca552cd1650', + chainId: 1 + }, + { + address: '0xCa9A0F02c0589D550f06F78AfF604A5405b90448', + fromVersion: 1, + fromImageHash: '0xbaf93699b3cb6214742cd6cccae0a6d1a0240ca4e03bf491b15707cdf46eca24', + chainId: 1 + }, + { + address: '0xd5b1C31f7626A8Bd206D744128dFE6401dd7D7F6', + fromVersion: 1, + fromImageHash: '0xcf813d102720b67781e784e852e624f86a5bb92a9a37234e2a89390b0b814480', + chainId: 1 + }, + { + address: '0xEf87203423cA064A44CE2661Daf93051e2F423a2', + fromVersion: 1, + fromImageHash: '0x338a2e6e1533e902f698e4623afc9b78f7c1b955f1e9c99ff4a4ee914dbbb401', + chainId: 1 + } + ] + + for (const { address, fromVersion, fromImageHash, chainId } of migrations) { + const [arweaveMigration, sessionsMigration] = await Promise.all([ + arweave.getMigration(address, fromImageHash, fromVersion, chainId), + sessions.getMigration(address, fromImageHash, fromVersion, chainId) + ]) + + expect(arweaveMigration).to.deep.equal(sessionsMigration) + } + }) +})