Skip to content
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

feat/bic-333_mee_support #166

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
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
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
PRIVATE_KEY=
CHAIN_ID=84532
ALT_CHAIN_ID=11155111
MAINNET_CHAIN_ID=10
RPC_URL=
BUNDLER_URL=
BICONOMY_SDK_DEBUG=false
PAYMASTER_URL=
PIMLICO_API_KEY=
TENDERLY_API_KEY=
TENDERLY_ACCOUNT_SLUG=
TENDERLY_PROJECT_SLUG=
TENDERLY_PROJECT_SLUG=
RUN_PAID_TESTS=false
35 changes: 35 additions & 0 deletions .github/workflows/funded-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: paid-tests
on:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
paid-tests:
name: paid-tests
permissions: write-all
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-paid-tests
cancel-in-progress: true
steps:
- uses: actions/setup-node@v4
with:
node-version: 22

- uses: actions/checkout@v4

- name: Install dependencies
uses: ./.github/actions/install-dependencies

- name: Run the tests
run: bun run test
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
PIMLICO_API_KEY: ${{ secrets.PIMLICO_API_KEY }}
PAYMASTER_URL: ${{ secrets.PAYMASTER_URL }}
BUNDLER_URL: ${{ secrets.BUNDLER_URL }}
CHAIN_ID: 84532
MAINNET_CHAIN_ID: 10
RUN_PAID_TESTS: true
CI: true

2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ jobs:
PAYMASTER_URL: ${{ secrets.PAYMASTER_URL }}
BUNDLER_URL: ${{ secrets.BUNDLER_URL }}
CHAIN_ID: 84532
ALT_CHAIN_ID: 11155420
MAINNET_CHAIN_ID: 10
CI: true
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,7 @@ dist

docs

sessionStorageData
sessionStorageData

# Data from scraping scripts
.data
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @biconomy/sdk

## 0.0.25

### Patch Changes

- Added Mee Client

## 0.0.24

### Patch Changes
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ bun install --frozen-lockfile
# Run all tests
bun run test

# Run tests for a specific module
bun run test -t=smartSessions
# Run tests for a specific subset of tests (by test description)
bun run test -t=mee
```

For detailed information about the testing framework, network configurations, and debugging guidelines, please refer to our [Testing Documentation](./src/test/README.md).
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"noExplicitAny": "warn"
},
"style": {
"noUnusedTemplateLiteral": "warn"
"noUnusedTemplateLiteral": "warn",
"noNonNullAssertion": "off"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@biconomy/sdk",
"version": "0.0.24",
"version": "0.0.25",
"author": "Biconomy",
"repository": "github:bcnmy/sdk",
"main": "./dist/_cjs/index.js",
Expand Down Expand Up @@ -102,6 +102,7 @@
"test:watch": "bun run test dev",
"playground": "RUN_PLAYGROUND=true vitest -c ./src/test/vitest.config.ts -t=playground",
"playground:watch": "RUN_PLAYGROUND=true bun run test -t=playground --watch",
"fetch:tokenMap": "bun run scripts/fetch:tokenMap.ts && bun run lint:fix",
"size": "size-limit",
"docs": "typedoc --tsconfig ./tsconfig/tsconfig.esm.json",
"docs:deploy": "bun run docs && gh-pages -d docs",
Expand Down
209 changes: 209 additions & 0 deletions scripts/fetch:tokenMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import fs from "node:fs"
import path from "node:path"
import { getAddress, isHex } from "viem"
import { baseSepolia } from "viem/chains"
import coinDataFromJson from "../.data/coinData.json"
import coinIdsFromJson from "../.data/coinIds.json"
import networkIdMap from "../.data/networkIdMap.json"

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async function writeJsonToFile(data: any, filename: string) {
// Create directory path if it doesn't exist
const dirname = path.dirname(filename)
fs.mkdirSync(dirname, { recursive: true })
fs.writeFileSync(filename, JSON.stringify(data, null, 2))
}

async function writeTsToFile(data: string, filename: string) {
// Create directory path if it doesn't exist
const dirname = path.dirname(filename)
fs.mkdirSync(dirname, { recursive: true })
fs.writeFileSync(filename, data)
}

async function getErc20CoinsByMarketCap(limit = 200): Promise<string[]> {
if (coinIdsFromJson.length > 0) return coinIdsFromJson

const COINS_TO_OMIT_FROM_COIN_DATA = coinDataFromJson
.map((coin) => coin?.id)
.filter(Boolean)

const COINS_TO_OMIT = ["ethereum", ...COINS_TO_OMIT_FROM_COIN_DATA]
const fetchResponse = await fetch(
`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&category=ethereum-ecosystem&order=market_cap_desc&per_page=${limit}&page=1&sparkline=false`,
{ method: "GET", headers: { accept: "application/json" } }
)
const coinResponse = await fetchResponse.json()
const coinIds = coinResponse
.filter((coin) => !COINS_TO_OMIT.includes(coin.id))
.map((coin) => coin.id)
writeJsonToFile(coinIds, path.join(__dirname, "../.data/coinIds.json"))
return coinIds
}

type Coin = {
id: string
symbol: string
name: string
platforms: Record<string, string>
}
async function getCoinDataById(coinIds_: string[]): Promise<Coin[]> {
const COINS_TO_OMIT = coinDataFromJson.map((coin) => coin?.id).filter(Boolean)
const coinsToFetch = coinIds_.filter(
(coinId) => !COINS_TO_OMIT.includes(coinId)
)
const coinsAlreadyFetched = coinDataFromJson.filter((coin) => coin?.id)

const coinResponses = []
// Run promises in sequence
for (const coinId of coinsToFetch) {
try {
const coinResponse = await fetch(
`https://api.coingecko.com/api/v3/coins/${coinId}`,
{
method: "GET",
headers: { accept: "application/json" }
}
)
await new Promise((resolve) => setTimeout(resolve, 3000)) // Sleep for 3 seconds to avoid rate limiting
// @ts-ignore
coinResponses.push(await coinResponse.json())
} catch (error) {
// Likely rate limited. Skip this coin and continue
console.error(`Error fetching coin data for ${coinId}:`, error)
}
}

const coinData = coinResponses.map(({ id, symbol, name, platforms }) => ({
id,
symbol,
name,
platforms
}))

const newCoinData = [...coinsAlreadyFetched, ...coinData].filter(
(v) => Object.keys(v ?? {}).length > 0
)

writeJsonToFile(newCoinData, path.join(__dirname, "../.data/coinData.json"))

// @ts-ignore
return newCoinData
}

type Networks = Record<string, number>
async function getNetworkIds(): Promise<Networks> {
if (Object.keys(networkIdMap).length > 0) return networkIdMap
const fetchResponse = await fetch(
"https://api.coingecko.com/api/v3/asset_platforms",
{ method: "GET", headers: { accept: "application/json" } }
)
const networks = await fetchResponse.json()
const newNetworkIdMap = networks.reduce((acc, network) => {
const networkId = Number(network?.chain_identifier ?? 0)
const networkName = network?.id
if (!!networkId && !!networkName) {
acc[networkName] = networkId
}
return acc
}, {})
writeJsonToFile(
newNetworkIdMap,
path.join(__dirname, "../.data/networkIdMap.json")
)
return newNetworkIdMap
}

type FinalisedCoin = {
id: string
symbol: string
name: string
networks: Record<string, string>
}

function sanitiseCoins(networks: Networks, coinData: Coin[]): FinalisedCoin[] {
const HARDCODE = {
usdc: {
[baseSepolia.id]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
}
}

// @ts-ignore
return coinData
.filter(Boolean)
.map(({ id, symbol, name, platforms }) => {
const networkNames = Object.keys(platforms ?? {})
const sanitisedNetworks = networkNames.reduce(
(acc, platform) => {
const networkId = networks[platform]
const address = platforms[platform]
if (networkId && isHex(address)) acc[networkId] = getAddress(address)
return acc
},
(HARDCODE[symbol] ?? {}) as Record<string, string>
)

if (
!id ||
!symbol ||
!name ||
!platforms ||
Object.keys(sanitisedNetworks ?? {}).length <= 1
) {
return undefined
}
return {
id,
symbol,
name,
networks: sanitisedNetworks
}
})
.filter(Boolean)
}

async function generateTokenConstants(coins: FinalisedCoin[]) {
const warning =
"// N.B. This file is auto-generated by the fetch:tokenMap.ts script. Do not edit it manually. \n// Instead, edit the script and run it again, or hardcode your new tokens in the index file that imports this file"

const startOfFile = `import { getMultichainContract } from "../../account/utils/getMultichainContract";\nimport { erc20Abi } from "viem"\n\n`

const tokenConstants = coins
.map((coin) => {
const { symbol, networks } = coin

const safeId = symbol
.replace(/[^a-zA-Z0-9]/g, "_") // Replace any non-alphanumeric chars with underscore
.replace(/^[0-9]/, "_$&") // If starts with number, prefix with underscore
.toUpperCase()

return `export const mc${safeId} = getMultichainContract<typeof erc20Abi>({
abi: erc20Abi,
deployments: [${Object.entries(networks)
.map(([networkId, address]) => `['${address}', ${networkId}]`)
.join(",\n ")}]
})`
})
.join("\n\n")

writeTsToFile(
`${warning}\n\n${startOfFile}\n\n${tokenConstants}`,
path.join(__dirname, "../src/sdk/constants/tokens/__AUTO_GENERATED__.ts")
)
}

async function main() {
const coinIds = await getErc20CoinsByMarketCap(200)
const coinData = await getCoinDataById(coinIds)
console.log("coinData.length", coinData.length)

const networks = await getNetworkIds()
const coins = sanitiseCoins(networks, coinData)
console.log("coins.length", coins.length)

await writeJsonToFile(coins, path.join(__dirname, "../.data/coins.json"))

await generateTokenConstants(coins)
}

main().catch((err) => console.error(err))
59 changes: 59 additions & 0 deletions src/sdk/account/decorators/buildBridgeInstructions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Address, Chain, LocalAccount } from "viem"
import { base } from "viem/chains"
import { beforeAll, describe, expect, it } from "vitest"
import { toNetwork } from "../../../test/testSetup"
import type { NetworkConfig } from "../../../test/testUtils"
import { type MeeClient, createMeeClient } from "../../clients/createMeeClient"
import { mcUSDC } from "../../constants/tokens"
import {
type MultichainSmartAccount,
toMultichainNexusAccount
} from "../toMultiChainNexusAccount"
import { toAcrossPlugin } from "../utils/toAcrossPlugin"
import buildBridgeInstructions from "./buildBridgeInstructions"
import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance"

describe("mee:buildBridgeInstructions", () => {
let network: NetworkConfig
let eoaAccount: LocalAccount
let paymentChain: Chain
let paymentToken: Address
let mcNexus: MultichainSmartAccount
let meeClient: MeeClient

beforeAll(async () => {
network = await toNetwork("MAINNET_FROM_ENV_VARS")

paymentChain = network.chain
paymentToken = network.paymentToken!
eoaAccount = network.account!

mcNexus = await toMultichainNexusAccount({
chains: [base, paymentChain],
signer: eoaAccount
})

meeClient = createMeeClient({ account: mcNexus })
})

it("should call the bridge with a unified balance", async () => {
const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC)
const payload = await buildBridgeInstructions({
account: mcNexus,
amount: 1n,
bridgingPlugins: [toAcrossPlugin()],
toChain: base,
unifiedBalance
})

expect(payload).toHaveProperty("meta")
expect(payload).toHaveProperty("instructions")
expect(payload.instructions.length).toBeGreaterThan(0)
expect(payload.meta.bridgingInstructions.length).toBeGreaterThan(0)
expect(payload.meta.bridgingInstructions[0]).toHaveProperty("userOp")
expect(payload.meta.bridgingInstructions[0].userOp).toHaveProperty("calls")
expect(
payload.meta.bridgingInstructions[0].userOp.calls.length
).toBeGreaterThan(0)
})
})
Loading
Loading