Skip to content

Commit

Permalink
Merge branch 'feature/cross-vm' into cf/create-coa
Browse files Browse the repository at this point in the history
  • Loading branch information
chasefleming authored Feb 6, 2025
2 parents f0fa6f0 + d11dd4e commit 444c47a
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 55 deletions.
110 changes: 62 additions & 48 deletions packages/fcl-ethereum-provider/src/accounts/account-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {mockUser} from "../__mocks__/fcl"
import * as fcl from "@onflow/fcl"
import * as rlp from "@onflow/rlp"
import {CurrentUser} from "@onflow/typedefs"
import {ChainIdStore, NetworkManager} from "../network/network-manager"
import {BehaviorSubject, Subject} from "../util/observable"

jest.mock("@onflow/fcl", () => {
const fcl = jest.requireActual("@onflow/fcl")
Expand All @@ -24,35 +26,41 @@ const mockFcl = jest.mocked(fcl)
const mockQuery = jest.mocked(fcl.query)

describe("AccountManager", () => {
let networkManager: jest.Mocked<NetworkManager>
let userMock: ReturnType<typeof mockUser>

beforeEach(() => {
jest.clearAllMocks()

const chainId$ = new BehaviorSubject<number | null>(747)
networkManager = {
$chainId: chainId$,
getChainId: () => chainId$.getValue(),
} as any as jest.Mocked<NetworkManager>
userMock = mockUser()
})

it("should initialize with null COA address", async () => {
const user = mockUser()
const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)
expect(await accountManager.getCOAAddress()).toBeNull()
expect(await accountManager.getAccounts()).toEqual([])
})

it("should reset state when the user is not logged in", async () => {
const user = mockUser()

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

expect(await accountManager.getCOAAddress()).toBeNull()
expect(await accountManager.getAccounts()).toEqual([])
})

it("should fetch and update COA address when user logs in", async () => {
const user = mockUser()
mockQuery.mockResolvedValue("0x123")

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

expect(await accountManager.getCOAAddress()).toBe(null)

user.set!({addr: "0x1"} as CurrentUser)
userMock.set!({addr: "0x1"} as CurrentUser)

expect(await accountManager.getCOAAddress()).toBe("0x123")
expect(await accountManager.getAccounts()).toEqual(["0x123"])
Expand Down Expand Up @@ -82,26 +90,24 @@ describe("AccountManager", () => {
})

it("should not update COA address if user has not changed", async () => {
const user = mockUser()
mockQuery.mockResolvedValue("0x123")

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

user.set!({addr: "0x1"} as CurrentUser)
userMock.set!({addr: "0x1"} as CurrentUser)

await new Promise(setImmediate)

expect(await accountManager.getCOAAddress()).toBe("0x123")
expect(fcl.query).toHaveBeenCalledTimes(1)

user.set!({addr: "0x1"} as CurrentUser)
userMock.set!({addr: "0x1"} as CurrentUser)

expect(await accountManager.getCOAAddress()).toBe("0x123")
expect(fcl.query).toHaveBeenCalledTimes(1) // Should not have fetched again
})

it("should not update COA address if fetch is outdated when user changes", async () => {
const user = mockUser()
mockQuery.mockResolvedValue("0x123")

mockQuery
Expand All @@ -112,39 +118,36 @@ describe("AccountManager", () => {
// 2nd fetch: immediate
.mockResolvedValueOnce("0x456")

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

await user.set!({addr: "0x1"} as CurrentUser)
await user.set!({addr: "0x2"} as CurrentUser)
await userMock.set!({addr: "0x1"} as CurrentUser)
await userMock.set!({addr: "0x2"} as CurrentUser)

// The second fetch (for address 0x2) is the latest, so "0x456"
expect(await accountManager.getCOAAddress()).toBe("0x456")
})

it("should throw if COA address fetch fails", async () => {
const user = mockUser()
mockQuery.mockRejectedValueOnce(new Error("Fetch failed"))

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

await user.set!({addr: "0x1"} as CurrentUser)
await userMock.set!({addr: "0x1"} as CurrentUser)

await expect(accountManager.getCOAAddress()).rejects.toThrow("Fetch failed")
})

it("should handle user changes correctly", async () => {
const user = mockUser()

mockQuery
.mockResolvedValueOnce("0x123") // for user 0x1
.mockResolvedValueOnce("0x456") // for user 0x2

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

await user.set({addr: "0x1"} as CurrentUser)
await userMock.set({addr: "0x1"} as CurrentUser)
expect(await accountManager.getCOAAddress()).toBe("0x123")

await user.set({addr: "0x2"} as CurrentUser)
await userMock.set({addr: "0x2"} as CurrentUser)

await new Promise(setImmediate)
expect(await accountManager.getCOAAddress()).toBe("0x456")
Expand All @@ -153,14 +156,12 @@ describe("AccountManager", () => {
it("should call the callback with updated accounts in subscribe", async () => {
mockQuery.mockResolvedValue("0x123")

const user = mockUser()

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

const callback = jest.fn()
accountManager.subscribe(callback)

user.set({addr: "0x1"} as CurrentUser)
userMock.set({addr: "0x1"} as CurrentUser)

await new Promise(setImmediate)

Expand All @@ -169,11 +170,10 @@ describe("AccountManager", () => {

it("should reset accounts in subscribe if user is not authenticated", async () => {
mockQuery.mockResolvedValue("0x123")
const user = mockUser()

const callback = jest.fn()

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

accountManager.subscribe(callback)

Expand All @@ -185,11 +185,11 @@ describe("AccountManager", () => {
it("should call the callback when COA address is updated", async () => {
const callback = jest.fn()

const user = mockUser({addr: "0x1"} as CurrentUser)

mockQuery.mockResolvedValueOnce("0x123")

const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(userMock.mock, networkManager)

userMock.set({addr: "0x1"} as CurrentUser)

accountManager.subscribe(callback)

Expand All @@ -199,23 +199,31 @@ describe("AccountManager", () => {
})

it("should return an empty array when COA address is null", async () => {
const {mock: user} = mockUser()
const accountManager = new AccountManager(user)
const accountManager = new AccountManager(userMock.mock, networkManager)
expect(await accountManager.getAccounts()).toEqual([])
})

it("should return COA address array when available", async () => {
mockQuery.mockResolvedValueOnce("0x123")
const {mock: user} = mockUser({addr: "0x1"} as CurrentUser)
userMock.set({addr: "0x1"} as CurrentUser)

const accountManager = new AccountManager(user)
const accountManager = new AccountManager(userMock.mock, networkManager)

expect(await accountManager.getAccounts()).toEqual(["0x123"])
})
})

describe("send transaction", () => {
let networkManager: jest.Mocked<NetworkManager>
let $mockChainId: BehaviorSubject<number | null>

beforeEach(() => {
$mockChainId = new BehaviorSubject<number | null>(747)
networkManager = {
$chainId: $mockChainId,
getChainId: () => $mockChainId.getValue(),
} as any as jest.Mocked<NetworkManager>

jest.clearAllMocks()
})

Expand All @@ -238,7 +246,7 @@ describe("send transaction", () => {
jest.mocked(fcl.query).mockResolvedValue("0x1234")

const user = mockUser({addr: "0x4444"} as CurrentUser).mock
const accountManager = new AccountManager(user)
const accountManager = new AccountManager(user, networkManager)

const tx = {
to: "0x1234",
Expand Down Expand Up @@ -267,6 +275,9 @@ describe("send transaction", () => {
})

test("send transaction testnet", async () => {
// Set chainId to testnet
$mockChainId.next(646)

const mockTxResult = {
onceExecuted: jest.fn().mockResolvedValue({
events: [
Expand All @@ -285,7 +296,7 @@ describe("send transaction", () => {
jest.mocked(fcl.query).mockResolvedValue("0x1234")

const user = mockUser({addr: "0x4444"} as CurrentUser)
const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(user.mock, networkManager)

const tx = {
to: "0x1234",
Expand Down Expand Up @@ -325,7 +336,7 @@ describe("send transaction", () => {
jest.mocked(fcl.query).mockResolvedValue("0x1234")

const user = mockUser({addr: "0x4444"} as CurrentUser)
const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(user.mock, networkManager)

const tx = {
to: "0x1234",
Expand All @@ -334,7 +345,7 @@ describe("send transaction", () => {
data: "0x1234",
nonce: "0",
gas: "0",
chainId: "646",
chainId: "747",
}

await expect(accountManager.sendTransaction(tx)).rejects.toThrow(
Expand All @@ -345,7 +356,7 @@ describe("send transaction", () => {
test("throws error if from address does not match user address", async () => {
jest.mocked(fcl.query).mockResolvedValue("0x1234")
const user = mockUser({addr: "0x4444"} as CurrentUser)
const accountManager = new AccountManager(user.mock)
const accountManager = new AccountManager(user.mock, networkManager)

const tx = {
to: "0x1234",
Expand All @@ -358,14 +369,15 @@ describe("send transaction", () => {
}

await expect(accountManager.sendTransaction(tx)).rejects.toThrow(
`From address does not match authenticated user address.\nUser: 0x1234\nFrom: 0x4567`
`Chain ID does not match the current network. Expected: 747, Received: 646`
)

expect(fcl.mutate).not.toHaveBeenCalled()
})
})

describe("signMessage", () => {
let networkManager: jest.Mocked<NetworkManager>
let accountManager: AccountManager
let user: ReturnType<typeof mockUser>["mock"]
let updateUser: ReturnType<typeof mockUser>["set"]
Expand All @@ -374,7 +386,12 @@ describe("signMessage", () => {
jest.clearAllMocks()
;({mock: user, set: updateUser} = mockUser({addr: "0x123"} as CurrentUser))
jest.mocked(fcl.query).mockResolvedValue("0xCOA1")
accountManager = new AccountManager(user)
const $mockChainId = new BehaviorSubject<number | null>(747)
networkManager = {
$chainId: $mockChainId,
getChainId: () => $mockChainId.getValue(),
} as any as jest.Mocked<NetworkManager>
accountManager = new AccountManager(user, networkManager)
})

it("should throw an error if the COA address is not available", async () => {
Expand Down Expand Up @@ -419,10 +436,7 @@ describe("signMessage", () => {
})

it("should throw an error if signUserMessage returns an empty array", async () => {
accountManager["coaAddress"] = "0xCOA1"

user.signUserMessage = jest.fn().mockResolvedValue([])

user.signUserMessage.mockResolvedValue([])
await expect(
accountManager.signMessage("Test message", "0xCOA1")
).rejects.toThrow("Failed to sign message")
Expand Down
29 changes: 22 additions & 7 deletions packages/fcl-ethereum-provider/src/accounts/account-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
switchMap,
} from "../util/observable"
import {EthSignatureResponse} from "../types/eth"
import {NetworkManager} from "../network/network-manager"
import {formatChainId, getContractAddress} from "../util/eth"

const CREATE_COA_TX = `
import "EVM"
Expand Down Expand Up @@ -53,7 +55,10 @@ export class AccountManager {
error: null,
})

constructor(private user: typeof fcl.currentUser) {
constructor(
private user: typeof fcl.currentUser,
private networkManager: NetworkManager
) {
// Create an observable from the user
const $user = new Observable<CurrentUser>(subscriber => {
return this.user.subscribe((currentUser: CurrentUser, error?: Error) => {
Expand Down Expand Up @@ -108,8 +113,13 @@ export class AccountManager {
}

private async fetchCOAFromFlowAddress(flowAddr: string): Promise<string> {
const chainId = await this.networkManager.getChainId()
if (!chainId) {
throw new Error("No active chain")
}

const cadenceScript = `
import EVM
import EVM from ${getContractAddress(ContractType.EVM, chainId)}
access(all)
fun main(address: Address): String? {
Expand Down Expand Up @@ -207,17 +217,22 @@ export class AccountManager {
chainId: string
}) {
// Find the Flow network based on the chain ID
const parsedChainId = parseInt(chainId)
const flowNetwork = Object.entries(FLOW_CHAINS).find(
([, chain]) => chain.eip155ChainId === parseInt(chainId)
([, chain]) => chain.eip155ChainId === parsedChainId
)?.[0] as FlowNetwork | undefined

if (!flowNetwork) {
throw new Error("Flow network not found for chain ID")
}

const evmContractAddress = fcl.withPrefix(
FLOW_CONTRACTS[ContractType.EVM][flowNetwork]
)
// Validate the chain ID
const currentChainId = await this.networkManager.getChainId()
if (parsedChainId !== currentChainId) {
throw new Error(
`Chain ID does not match the current network. Expected: ${currentChainId}, Received: ${parsedChainId}`
)
}

// Check if the from address matches the authenticated COA address
const expectedCOAAddress = await this.getCOAAddress()
Expand All @@ -228,7 +243,7 @@ export class AccountManager {
}

const txId = await fcl.mutate({
cadence: `import EVM from ${evmContractAddress}
cadence: `import EVM from ${getContractAddress(ContractType.EVM, parsedChainId)}
/// Executes the calldata from the signer's COA
///
Expand Down
Loading

0 comments on commit 444c47a

Please sign in to comment.