Skip to content

epic: Solana v1 #1484

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

Draft
wants to merge 35 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3a37fc2
Add validation or SVM addresses (#1478)
gsteenkamp89 Mar 5, 2025
da190a0
feat: custom wallet ui sidebar (#1470)
dohaki Mar 6, 2025
b7444d0
feat: solana wallet support (#1483)
dohaki Mar 10, 2025
0281dd0
Feat/improve wallet selection ux (#1489)
gsteenkamp89 Mar 10, 2025
502c26d
Feat/generate routes for solana (#1485)
gsteenkamp89 Mar 11, 2025
891618d
Merge branch 'master' into EPIC-solana-v1
dohaki Apr 7, 2025
2da4116
fix: sepolia universal swap routes
dohaki Apr 11, 2025
8dd5846
Merge branch 'master' into EPIC-solana-v1
gsteenkamp89 Apr 17, 2025
27f0776
Merge branch 'master' into EPIC-solana-v1
dohaki May 8, 2025
05255d4
Merge branch 'master' into EPIC-solana-v1
dohaki May 12, 2025
d16c216
feat: evm svm bridge action hooks (#1543)
dohaki May 16, 2025
de9eda8
feat: support svm useBalance hooks (#1558)
dohaki May 17, 2025
3173da7
feat(api): support svm (#1589)
dohaki May 20, 2025
1265b86
fix: useBalance svm strategy
dohaki May 20, 2025
e0ee620
Merge branch 'master' into EPIC-solana-v1
dohaki May 20, 2025
c41575a
feat: evm/svm recipient edge cases (#1579)
dohaki May 21, 2025
b3848ee
feat: enable solana mainnet + fixes (#1592)
dohaki May 21, 2025
9f9c8f6
feat: upgrade spoke pool verifier (#1593)
dohaki May 26, 2025
9723fa5
Merge branch 'master' into EPIC-solana-v1
dohaki May 26, 2025
2d7062b
bump
pxrl May 28, 2025
0752075
Drop redundant export
pxrl May 28, 2025
683efe1
bump sdk
pxrl May 28, 2025
b62f2ca
Fixes filldeadline & empty message for deposits to Solana (#1605)
gsteenkamp89 May 29, 2025
fa64e77
update sdk
gsteenkamp89 May 29, 2025
249d4f0
feat: solana featured bridge route banner (#1603)
dohaki Jun 3, 2025
dc9e712
Fix deposit (#1621)
gsteenkamp89 Jun 5, 2025
a0583ba
feat(deposit-tracking): implement evm & svm strategies for tracking d…
gsteenkamp89 Jun 10, 2025
30b2d96
Merge branch 'master' into EPIC-solana-v1
dohaki Jun 11, 2025
a127714
chore: clean up legacy `useBalance` hook
dohaki Jun 11, 2025
ff9c5fe
fix: relayer gas fees for solana (#1626)
dohaki Jun 17, 2025
7fc7731
improve: disable speedup option for specific chains (#1622)
james-a-morris Jun 17, 2025
b604aac
fix: use address param instead of depositor when querying deposits (#…
melisaguevara Jun 25, 2025
805ff8a
Support updated SpokePool program IDL (#1633)
gsteenkamp89 Jun 30, 2025
c8f01d1
use solana's logic for detecting installed wallets (#1655)
gsteenkamp89 Jul 4, 2025
a172c81
Fix SVM WalletConnect adapter (#1656)
gsteenkamp89 Jul 7, 2025
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: 1 addition & 4 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import { Preview } from "@storybook/react";
import { MemoryRouter } from "react-router-dom";

import { default as GlobalStyles } from "../src/components/GlobalStyles/GlobalStyles";
import { OnboardContext, useOnboardManager } from "../src/hooks/useOnboard";

const preview: Preview = {
decorators: [
(Story) => (
<MemoryRouter initialEntries={["/"]}>
<GlobalStyles />
<OnboardContext.Provider value={useOnboardManager()}>
<Story />
</OnboardContext.Provider>
<Story />
</MemoryRouter>
),
],
Expand Down
46 changes: 43 additions & 3 deletions api/_address.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
import { isAddress as isEvmAddress } from "viem";
import { isAddress as isSvmAddress } from "@solana/kit";
import { Address, Hex, padHex, toBytes, toHex, trim } from "viem";
import {
getAddressDecoder,
getAddressEncoder,
isAddress as isSvmAddress,
} from "@solana/kit";
import { isAddress } from "ethers/lib/utils";

// exports
export { isSvmAddress, isEvmAddress };
export { isSvmAddress };

// utils
export function isEvmAddress(value: string): value is Address {
return isAddress(value);
}

export function svmToHex(pubkey: string): Hex {
if (!isSvmAddress(pubkey)) {
throw new Error("Invalid SVM Address");
}
const bytes = getAddressEncoder().encode(pubkey);
return toHex(new Uint8Array(bytes));
}

export function hexToBase58(address: Address) {
if (!isEvmAddress(address)) {
throw new Error("Invalid EVM Address");
}
const bytes = trim(toBytes(address));
return getAddressDecoder().decode(bytes);
}

export function toBytes32(value: string) {
if (isSvmAddress(value)) {
// byte length already checked at this stage
return svmToHex(value);
}
if (isEvmAddress(value)) {
return padHex(value, {
size: 32,
dir: "left",
});
}
throw new Error("Invalid Address type. Must be valid EVM or SVM address");
}
196 changes: 196 additions & 0 deletions api/_balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { BigNumber, providers } from "ethers";
import * as sdk from "@across-protocol/sdk";
import { ERC20__factory } from "@across-protocol/contracts/dist/typechain";

import { getSvmProvider, getProvider } from "./_providers";
import { BLOCK_TAG_LAG } from "./_constants";
import { getMulticall3, callViaMulticall3 } from "./_multicall";

/**
* Resolves the balance of a given token at a provided address.
* @param chainId The chain Id to query against
* @param account A valid Web3 wallet address
* @param token The valid token address on the given `chainId`.
* @returns A promise that resolves to the BigNumber of the balance
*/
export async function getBalance(
chainId: string | number,
account: string,
token: string,
blockTag?: string | number
): Promise<BigNumber> {
if (sdk.utils.chainIsSvm(Number(chainId))) {
return getSvmBalance(chainId, account, token);
}
return sdk.utils.getTokenBalance(
account,
token,
getProvider(Number(chainId)),
blockTag ?? BLOCK_TAG_LAG
);
}

async function getSvmBalance(
chainId: string | number,
account: string,
token: string
) {
const tokenMint = sdk.utils.toAddressType(token);
const owner = sdk.utils.toAddressType(account);
const svmProvider = getSvmProvider(Number(chainId)).createRpcClient();

if (tokenMint.isZeroAddress()) {
const address = owner.forceSvmAddress().toV2Address();
const balance = await svmProvider.getBalance(address).send();
return BigNumber.from(balance.value);
}

// Get the associated token account address
const tokenAccount = await sdk.arch.svm.getAssociatedTokenAddress(
owner.forceSvmAddress(),
tokenMint.forceSvmAddress()
);

let balance: BigNumber;
try {
// Get token account info
const tokenAccountInfo = await svmProvider
.getTokenAccountBalance(tokenAccount)
.send();
balance = BigNumber.from(tokenAccountInfo.value.amount);
} catch (error) {
// If token account doesn't exist or other error, return 0 balance
balance = BigNumber.from(0);
}
return balance;
}

export async function getBatchBalance(
chainId: string | number,
addresses: string[],
tokenAddresses: string[],
blockTag: providers.BlockTag = "latest"
) {
if (sdk.utils.chainIsSvm(Number(chainId))) {
return getBatchSvmBalance(chainId, addresses, tokenAddresses);
}
return getBatchBalanceViaMulticall3(
chainId,
addresses,
tokenAddresses,
blockTag
);
}

/**
* Fetches the balances for an array of addresses on a particular chain, for a particular erc20 token
* @param chainId The blockchain Id to query against
* @param addresses An array of valid Web3 wallet addresses
* @param tokenAddress The valid ERC20 token address on the given `chainId` or ZERO_ADDRESS for native balances
* @param blockTag Block to query from, defaults to latest block
* @returns a Promise that resolves to an array of BigNumbers
*/
export async function getBatchBalanceViaMulticall3(
chainId: string | number,
addresses: string[],
tokenAddresses: string[],
blockTag: providers.BlockTag = "latest"
): Promise<{
blockNumber: providers.BlockTag;
balances: Record<string, Record<string, string>>;
}> {
const chainIdAsInt = Number(chainId);
const provider = getProvider(chainIdAsInt);

const multicall3 = getMulticall3(chainIdAsInt, provider);

if (!multicall3) {
throw new Error("No Multicall3 deployed on this chain");
}

let calls: Parameters<typeof callViaMulticall3>[1] = [];

for (const tokenAddress of tokenAddresses) {
if (tokenAddress === sdk.constants.ZERO_ADDRESS) {
// For native currency
calls.push(
...addresses.map((address) => ({
contract: multicall3,
functionName: "getEthBalance",
args: [address],
}))
);
} else {
// For ERC20 tokens
const erc20Contract = ERC20__factory.connect(tokenAddress, provider);
calls.push(
...addresses.map((address) => ({
contract: erc20Contract,
functionName: "balanceOf",
args: [address],
}))
);
}
}

const inputs = calls.map(({ contract, functionName, args }) => ({
target: contract.address,
callData: contract.interface.encodeFunctionData(functionName, args),
}));

const [blockNumber, results] = await multicall3.callStatic.aggregate(inputs, {
blockTag,
});

const decodedResults = results.map((result, i) =>
calls[i].contract.interface.decodeFunctionResult(
calls[i].functionName,
result
)
);

let balances: Record<string, Record<string, string>> = {};

let resultIndex = 0;
for (const tokenAddress of tokenAddresses) {
addresses.forEach((address) => {
if (!balances[address]) {
balances[address] = {};
}
balances[address][tokenAddress] = decodedResults[resultIndex].toString();
resultIndex++;
});
}

return {
blockNumber: blockNumber.toNumber(),
balances,
};
}

export async function getBatchSvmBalance(
chainId: string | number,
addresses: string[],
tokenAddresses: string[],
blockTag: providers.BlockTag = "latest"
) {
const balances: Record<string, Record<string, string>> = {};

for (const tokenAddress of tokenAddresses) {
const callsPerAddress = addresses.map((address) =>
getSvmBalance(chainId, address, tokenAddress)
);
const results = await Promise.all(callsPerAddress);
for (const [index, result] of Object.entries(results)) {
const address = addresses[Number(index)];
if (!balances[address]) {
balances[address] = {};
}
balances[address][tokenAddress] = result.toString();
}
}
return {
blockNumber: blockTag,
balances,
};
}
5 changes: 2 additions & 3 deletions api/_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ export const CG_CONTRACTS_DEFERRED_TO_ID = new Set([
TOKEN_SYMBOLS_MAP.GHO.addresses[CHAIN_IDs.MAINNET],
...Object.values(TOKEN_SYMBOLS_MAP["TATARA-USDC"].addresses),
TOKEN_SYMBOLS_MAP.BNB.addresses[CHAIN_IDs.MAINNET],
TOKEN_SYMBOLS_MAP.SOL.addresses[CHAIN_IDs.SOLANA],
TOKEN_SYMBOLS_MAP.SOL.addresses[CHAIN_IDs.SOLANA_DEVNET],
TOKEN_SYMBOLS_MAP.VLR.addresses[CHAIN_IDs.MAINNET],
TOKEN_SYMBOLS_MAP.SOL.addresses[CHAIN_IDs.SOLANA],
TOKEN_SYMBOLS_MAP.SOL.addresses[CHAIN_IDs.SOLANA_DEVNET],
Expand Down Expand Up @@ -332,9 +334,6 @@ export const DEFI_LLAMA_POOL_LOOKUP: Record<string, string> = {
"8f7b5b8c-09db-45e3-8938-f30115d34672",
};

export const DEFAULT_SIMULATED_RECIPIENT_ADDRESS =
"0xBb23Cd0210F878Ea4CcA50e9dC307fb0Ed65Cf6B";

export const DOMAIN_CALLDATA_DELIMITER = "0x1dc0de";

export const DEFAULT_LITE_CHAIN_USD_MAX_BALANCE = "250000";
Expand Down
28 changes: 24 additions & 4 deletions api/_fill-deadline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as sdk from "@across-protocol/sdk";

import { DEFAULT_FILL_DEADLINE_BUFFER_SECONDS } from "./_constants";
import { getSpokePool } from "./_utils";
import { getSpokePool } from "./_spoke-pool";
import { getSVMRpc } from "./_providers";

function getFillDeadlineBuffer(chainId: number) {
const bufferFromEnv = (
Expand All @@ -11,9 +14,26 @@ function getFillDeadlineBuffer(chainId: number) {
return Number(bufferFromEnv ?? DEFAULT_FILL_DEADLINE_BUFFER_SECONDS);
}

async function getCurrentTimeSvm(chainId: number): Promise<number> {
const rpc = getSVMRpc(chainId);
const timestamp = await rpc
.getSlot({
commitment: "confirmed",
})
.send()
.then((slot) => rpc.getBlockTime(slot).send());
return Number(timestamp);
}

export async function getFillDeadline(chainId: number): Promise<number> {
const fillDeadlineBuffer = getFillDeadlineBuffer(chainId);
const spokePool = getSpokePool(chainId);
const currentTime = await spokePool.callStatic.getCurrentTime();
return Number(currentTime) + fillDeadlineBuffer;
let currentTime: number;

if (sdk.utils.chainIsSvm(chainId)) {
currentTime = await getCurrentTimeSvm(chainId);
} else {
const spokePool = getSpokePool(chainId);
currentTime = (await spokePool.callStatic.getCurrentTime()).toNumber();
}
return currentTime + fillDeadlineBuffer;
}
70 changes: 70 additions & 0 deletions api/_gas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as sdk from "@across-protocol/sdk";
import { BigNumber, BigNumberish } from "ethers";
import { getDefaultRecipientAddress } from "./_recipient-address";

export function calcGasFeeDetails(params: {
gasPriceEstimate: sdk.gasPriceOracle.GasPriceEstimate;
nativeGasCost: BigNumberish;
opStackL1GasCost?: BigNumberish;
}) {
const { gasPriceEstimate, nativeGasCost, opStackL1GasCost } = params;

const gasPriceEstimateEvm =
gasPriceEstimate as sdk.gasPriceOracle.EvmGasPriceEstimate;
const gasPriceEstimateSvm =
gasPriceEstimate as sdk.gasPriceOracle.SvmGasPriceEstimate;

// EVM-based chains
if (
gasPriceEstimateEvm.maxFeePerGas &&
gasPriceEstimateEvm.maxPriorityFeePerGas
) {
const tokenGasCost = BigNumber.from(nativeGasCost)
.mul(gasPriceEstimateEvm.maxFeePerGas)
.add(opStackL1GasCost ?? 0);
return {
tokenGasCost,
opStackL1GasCost,
gasPrice: gasPriceEstimateEvm.maxFeePerGas,
nativeGasCost: BigNumber.from(nativeGasCost),
};
}

// SVM-based chains
if (
gasPriceEstimateSvm.baseFee &&
gasPriceEstimateSvm.microLamportsPerComputeUnit
) {
const tokenGasCost = gasPriceEstimateSvm.baseFee.add(
gasPriceEstimateSvm.microLamportsPerComputeUnit
.mul(nativeGasCost)
.div(1_000_000) // 1_000_000 microLamports/lamport.
);
return {
tokenGasCost,
opStackL1GasCost,
gasPrice: tokenGasCost,
nativeGasCost: BigNumber.from(nativeGasCost),
};
}

throw new Error(
"Can't compute gas fee details. Missing or incorrect gas price estimate."
);
}

export function getDepositArgsForCachedGasDetails(
chainId: number,
tokenAddress: string
) {
return {
amount: BigNumber.from(100),
inputToken: sdk.utils.toAddressType(sdk.constants.ZERO_ADDRESS).toBytes32(),
outputToken: sdk.utils.toAddressType(tokenAddress).toBytes32(),
recipientAddress: sdk.utils
.toAddressType(getDefaultRecipientAddress(chainId))
.toBytes32(),
originChainId: 0, // Shouldn't matter for simulation
destinationChainId: Number(chainId),
};
}
Loading