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

Merged
merged 14 commits into from
Jan 16, 2025
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @biconomy/sdk

## 0.0.29

### Patch Changes

- AbstractJS rebrand
- meeNode support
- useTestBundler

## 0.0.28

### Patch Changes
Expand Down
46 changes: 28 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![codecov](https://codecov.io/github/bcnmy/sdk/graph/badge.svg?token=DTdIR5aBDA)](https://codecov.io/github/bcnmy/sdk)

# SDK 🚀
# abstractJS 🚀

[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/bcnmy/sdk)

Expand Down Expand Up @@ -32,21 +32,31 @@ bun add @biconomy/sdk viem @rhinestone/module-sdk

2. **Basic Usage:**
```typescript
import { createSmartAccountClient } from "@biconomy/sdk";
import { http } from "viem";

const nexusClient = await createSmartAccountClient({
signer: account,
chain,
transport: http(),
bundlerTransport: http(bundlerUrl),
});

const hash = await nexusClient.sendTransaction({
calls: [{ to: "0x...", value: 1 }]
});

const { status, transactionHash } = await nexusClient.waitForTransactionReceipt({ hash });
import { toMultichainNexusAccount, mcUSDC } from "@biconomy/sdk";
import { base, optimism } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const eoaAccount = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`)
const mcNexus = await toMultichainNexusAccount({
chains: [base, optimism],
signer: eoaAccount
})
const meeClient = createMeeClient({ account: mcNexus })

const quote = await meeClient.getQuote({
instructions: [{
calls: [{ to: "0x...", value: 1n }],
chainId: base.id
}],
feeToken: {
address: mcUSDC.addressOn(base.id), // Token used to pay for the transaction
chainId: base.id // Chain where the payment will be processed
}
})

// Execute the quote and get back a transaction hash
// This sends the transaction to the network
const { hash } = await meeClient.executeQuote({ quote })
```

### Testing
Expand All @@ -67,8 +77,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.28",
"version": "0.0.29",
"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))
Loading
Loading