Skip to content
Merged
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
71 changes: 31 additions & 40 deletions .github/workflows/build-flow-emulator.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
name: 'Build and Push Flow Emulator Image'
name: 'Build and Push Mainnet Fork Emulator Image'

on:
push:
branches:
- main
pull_request:
branches:
- main
- v0

# Manual trigger, with a flag to decide if we also push
workflow_dispatch:
Expand All @@ -17,57 +14,51 @@ on:
default: false

env:
DOCKER_IMAGE_URL: us-west1-docker.pkg.dev/dl-flow-devex-staging/backend/flow-emulator:${{ github.sha }}
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/flow-emulator-fork

jobs:
build:
name: Build Flow Emulator Image
test-and-build:
name: E2E Tests and Build
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

outputs:
image-url: ${{ env.DOCKER_IMAGE_URL }}
packages: write

steps:
- name: 'Checkout'
uses: 'actions/checkout@v4'
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_PAT }}

- name: 'Build Flow Emulator image'
- name: Install Flow CLI
run: |
docker build -t "${{ env.DOCKER_IMAGE_URL }}" .

push:
name: Push Flow Emulator Image
runs-on: ubuntu-latest
needs: build
curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/v2.16.0/install.sh | bash
echo "${HOME}/.local/bin" >> $GITHUB_PATH

# Only run when manually dispatched *and* push=true
if: ${{ github.event_name == 'workflow_dispatch' && inputs.push == true }}
- name: Install flow dependencies
run: flow deps install

permissions:
contents: read
id-token: write
- name: Run e2e mainnet fork tests
run: ./local/e2e_mainnet_fork.sh

steps:
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v2'
- name: Log in to GHCR
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true)
uses: docker/login-action@v3
with:
workload_identity_provider: '${{ vars.BUILDER_WORKLOAD_IDENTITY_PROVIDER }}'
service_account: '${{ vars.BUILDER_SERVICE_ACCOUNT }}'
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 'Set up gcloud'
uses: 'google-github-actions/setup-gcloud@v1'
- name: Build and push flow-emulator-fork
uses: docker/build-push-action@v5
with:
project_id: ${{ vars.GAR_PROJECT_ID }}

- name: 'Push Flow Emulator image'
run: |
gcloud auth configure-docker ${{ vars.GAR_REGION }}-docker.pkg.dev
docker push "${{ needs.build.outputs.image-url }}"
context: .
file: Dockerfile.mainnet-fork
push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) }}
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest
cache-to: type=inline
26 changes: 26 additions & 0 deletions Dockerfile.mainnet-fork
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM debian:stable-slim

ENV APP_HOME=/app

RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates bash git jq \
&& rm -rf /var/lib/apt/lists/*

# Install flow CLI pinned to v2.16.0 (matches e2e workflow)
RUN curl -fsSL "https://raw.githubusercontent.com/onflow/flow-cli/v2.16.0/install.sh" | bash \
&& mv /root/.local/bin/flow /usr/local/bin/flow \
&& flow version

WORKDIR ${APP_HOME}
COPY . ${APP_HOME}
RUN chmod +x ${APP_HOME}/local/*.sh

# Pre-install Cadence dependencies at build time (source resolution only;
# mainnet fork state is fetched at container startup, not here)
RUN flow deps install --skip-alias --skip-deployments

EXPOSE 3569 8888

# Runtime: start emulator forked from mainnet.
# The fork state is fetched from access.mainnet.nodes.onflow.org at startup.
ENTRYPOINT ["flow", "emulator", "--fork", "mainnet", "--persist=false"]
25 changes: 25 additions & 0 deletions cadence/tests/transactions/grant_beta_to_self.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import "FlowYieldVaultsClosedBeta"

/// Single-signer variant: admin grants beta access to their own account.
/// Use when the admin is also the test user (avoids multi-sig complexity in shell scripts).
transaction() {
prepare(admin: auth(BorrowValue, Storage) &Account) {
let handle = admin.storage.borrow<auth(FlowYieldVaultsClosedBeta.Admin) &FlowYieldVaultsClosedBeta.AdminHandle>(
from: FlowYieldVaultsClosedBeta.AdminHandleStoragePath
) ?? panic("Missing AdminHandle at AdminHandleStoragePath")

let cap: Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge> =
handle.grantBeta(addr: admin.address)

let p = FlowYieldVaultsClosedBeta.UserBetaCapStoragePath

if let t = admin.storage.type(at: p) {
if t == Type<Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge>>() {
let _ = admin.storage.load<Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge>>(from: p)
} else {
panic("Unexpected type at UserBetaCapStoragePath: ".concat(t.identifier))
}
}
admin.storage.save(cap, to: p)
}
}
35 changes: 35 additions & 0 deletions cadence/tests/transactions/setup_ft_vault.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "ViewResolver"

/// Sets up a FungibleToken vault in the signer's storage if not already present,
/// publishing the standard receiver and metadata capabilities.
/// Works with any FT that implements FungibleTokenMetadataViews (including EVMVMBridgedTokens).
///
/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141)
/// @param contractName Name of the token contract

transaction(contractAddress: Address, contractName: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName)
?? panic("Cannot borrow ViewResolver for ".concat(contractName))

let vaultData = viewResolver.resolveContractView(
resourceType: nil,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as! FungibleTokenMetadataViews.FTVaultData?
?? panic("Cannot resolve FTVaultData for ".concat(contractName))

if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) != nil {
return // already set up
}

signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath)
signer.capabilities.unpublish(vaultData.receiverPath)
signer.capabilities.unpublish(vaultData.metadataPath)
let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)
let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)
signer.capabilities.publish(receiverCap, at: vaultData.receiverPath)
signer.capabilities.publish(metadataCap, at: vaultData.metadataPath)
}
}
41 changes: 41 additions & 0 deletions cadence/tests/transactions/transfer_ft_via_vault_data.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "ViewResolver"

/// Generic FungibleToken transfer that resolves storage/receiver paths via FTVaultData.
/// Works with any FT implementing FungibleTokenMetadataViews (including EVMVMBridgedTokens).
///
/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141)
/// @param contractName Name of the token contract (e.g. EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750)
/// @param amount Amount to transfer
/// @param to Recipient Cadence address (must already have receiver capability published)

transaction(contractAddress: Address, contractName: String, amount: UFix64, to: Address) {

let sentVault: @{FungibleToken.Vault}
let receiverPath: PublicPath

prepare(signer: auth(BorrowValue) &Account) {
let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName)
?? panic("Cannot borrow ViewResolver for ".concat(contractName))

let vaultData = viewResolver.resolveContractView(
resourceType: nil,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as! FungibleTokenMetadataViews.FTVaultData?
?? panic("Cannot resolve FTVaultData for ".concat(contractName))

let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(
from: vaultData.storagePath
) ?? panic("Cannot borrow vault from ".concat(vaultData.storagePath.toString()))

self.sentVault <- vaultRef.withdraw(amount: amount)
self.receiverPath = vaultData.receiverPath
}

execute {
let receiverRef = getAccount(to).capabilities.borrow<&{FungibleToken.Receiver}>(self.receiverPath)
?? panic("Cannot borrow receiver at ".concat(self.receiverPath.toString()))
receiverRef.deposit(from: <-self.sentVault)
}
}
41 changes: 41 additions & 0 deletions cadence/tests/transactions/update_flowalp_oracle_threshold.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "FlowALPv0"
import "BandOracleConnectors"
import "DeFiActions"
import "FungibleTokenConnectors"
import "FungibleToken"

/// Updates the FlowALP pool's price oracle with a larger staleThreshold.
///
/// Use in fork testing environments where Band oracle data may be up to
/// several hours old. The emulator fork uses mainnet state at a fixed
/// height; as real time advances the oracle data becomes stale.
///
/// @param staleThreshold: seconds beyond which oracle data is considered stale
/// Use 86400 (24h) for long-running fork test sessions.
///
/// Must be signed by the FlowALP pool owner (6b00ff876c299c61).
/// In fork mode, signature validation is disabled, so any key can be used.
transaction(staleThreshold: UInt64) {
let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool
let oracle: {DeFiActions.PriceOracle}

prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) {
self.pool = signer.storage.borrow<auth(FlowALPv0.EGovernance) &FlowALPv0.Pool>(from: FlowALPv0.PoolStoragePath)
?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)")
let defaultToken = self.pool.getDefaultToken()

let vaultCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)
let feeSource = FungibleTokenConnectors.VaultSource(min: nil, withdrawVault: vaultCap, uniqueID: nil)
self.oracle = BandOracleConnectors.PriceOracle(
unitOfAccount: defaultToken,
staleThreshold: staleThreshold,
feeSource: feeSource,
uniqueID: nil,
)
}

execute {
self.pool.setPriceOracle(self.oracle)
log("FlowALP oracle staleThreshold updated to \(staleThreshold)s")
}
}
25 changes: 14 additions & 11 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,6 @@
"type": "file",
"location": "local/emulator-account.pkey"
}

},
"testnet-flow-alp-deployer": {
"address": "426f0458ced60037",
Expand All @@ -1086,6 +1085,20 @@
"hashAlgorithm": "SHA2_256",
"resourceID": "projects/dl-flow-devex-staging/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1"
}
},
"mainnet-fork-flowalp": {
"address": "6b00ff876c299c61",
"key": {
"type": "file",
"location": "local/emulator-account.pkey"
}
},
"mainnet-fork-pyusd0-donor": {
"address": "24263c125b7770e0",
"key": {
"type": "file",
"location": "local/emulator-account.pkey"
}
}
},
"deployments": {
Expand Down Expand Up @@ -1248,16 +1261,6 @@
},
"mainnet-fork": {
"mainnet-fork-admin": [
{
"name": "MockOracle",
"args": [
{
"value": "A.6b00ff876c299c61.MOET.Vault",
"type": "String"
}
]
},
"MockSwapper",
"UInt64LinkedList",
"AutoBalancers",
"FlowYieldVaultsSchedulerRegistry",
Expand Down
Loading
Loading