Skip to content

Commit fc64c29

Browse files
feat(frontend): implement OneSec utils (#12610)
# Motivation - Adds a feature flag ONESEC_SWAP_ENABLED in src/frontend/src/env/rest/onesec.env.ts to gate the OneSec integration. - Implements src/frontend/src/lib/utils/onesec-swap.utils.ts, the foundational utility layer for OneSec bridging: - ICP_LEDGER_TO_TOKEN — static map from ICP ledger canister IDs to onesec-bridge token/config entries, built once from DEFAULT_CONFIG. - computeReceiveAmount — calculates the amount a user receives after deducting the transfer fee and protocol fee (both expressed in token units). - oneSecIcpSupportedTokens() — resolves to the set of ICP ledger canister IDs OneSec can handle as a source. - oneSecEvmSupportedTokens({ networkIds }) — resolves to lowercased ERC20 addresses supported by OneSec on the requested EVM networks, with per-network overrides (Arbitrum, Base, Ethereum mainnet). - oneSecCompatibleDestinations({ sourceToken, networkIds }) — returns the set of compatible destination identifiers by category (evm or icp) for a given source token, returning undefined when the feature is disabled or the token is unknown. - Adds 28 unit tests covering all exported symbols, including fee edge cases, network-specific address selection, and the ONESEC_SWAP_ENABLED guard.
1 parent 8152020 commit fc64c29

3 files changed

Lines changed: 468 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ONESEC_SWAP_ENABLED = true;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { ARBITRUM_MAINNET_NETWORK_ID } from '$env/networks/networks-evm/networks.evm.arbitrum.env';
2+
import { BASE_NETWORK_ID } from '$env/networks/networks-evm/networks.evm.base.env';
3+
import { ONESEC_SWAP_ENABLED } from '$env/rest/onesec.env';
4+
import type { Erc20Token } from '$eth/types/erc20';
5+
import { isIcToken } from '$icp/validation/ic-token.validation';
6+
import { ZERO } from '$lib/constants/app.constants';
7+
import type { NetworkId } from '$lib/types/network';
8+
import type { Token as AppToken } from '$lib/types/token';
9+
import { isNullish, nonNullish } from '@dfinity/utils';
10+
import { DEFAULT_CONFIG, type Token, type TokenConfig } from 'onesec-bridge';
11+
12+
export interface IcpLedgerEntry {
13+
token: Token;
14+
config: TokenConfig;
15+
}
16+
17+
const buildIcpLedgerMap = (): Record<string, IcpLedgerEntry> =>
18+
[...DEFAULT_CONFIG.tokens].reduce<Record<string, IcpLedgerEntry>>((map, [token, config]) => {
19+
const ledger = config.ledgerMainnet ?? config.ledger;
20+
if (nonNullish(ledger)) {
21+
map[ledger] = { token, config };
22+
}
23+
return map;
24+
}, {});
25+
26+
export const ICP_LEDGER_TO_TOKEN = buildIcpLedgerMap();
27+
28+
export const computeReceiveAmount = ({
29+
amount,
30+
transferFeeInUnits,
31+
protocolFeeInPercent,
32+
decimals
33+
}: {
34+
amount: bigint;
35+
transferFeeInUnits: bigint;
36+
protocolFeeInPercent: number;
37+
decimals: number;
38+
}): bigint => {
39+
const amountInTokens = Number(amount) / 10 ** decimals;
40+
const protocolFee = BigInt(
41+
Math.ceil(amountInTokens * (protocolFeeInPercent / 100) * 10 ** decimals)
42+
);
43+
const totalFee = transferFeeInUnits + protocolFee;
44+
return amount > totalFee ? amount - totalFee : ZERO;
45+
};
46+
47+
const getEvmAddressForNetwork = ({
48+
config,
49+
networkId
50+
}: {
51+
config: TokenConfig;
52+
networkId: NetworkId;
53+
}): string | undefined =>
54+
networkId === ARBITRUM_MAINNET_NETWORK_ID
55+
? config.erc20MainnetArbitrum
56+
: networkId === BASE_NETWORK_ID
57+
? config.erc20MainnetBase
58+
: (config.erc20MainnetEthereum ?? config.erc20Mainnet ?? config.erc20);
59+
60+
/**
61+
* Returns ICP ledger canister IDs of tokens supported by OneSec on the ICP side.
62+
*/
63+
export const oneSecIcpSupportedTokens = (): Promise<Set<string>> =>
64+
Promise.resolve(new Set(Object.keys(ICP_LEDGER_TO_TOKEN)));
65+
66+
/**
67+
* Returns ERC20 addresses (lowercased) of tokens supported by OneSec on the given EVM networks.
68+
*/
69+
export const oneSecEvmSupportedTokens = ({
70+
networkIds
71+
}: {
72+
networkIds: NetworkId[];
73+
}): Promise<Set<string>> => {
74+
const supported = new Set<string>();
75+
76+
for (const networkId of networkIds) {
77+
for (const [, config] of DEFAULT_CONFIG.tokens) {
78+
const address = getEvmAddressForNetwork({ config, networkId });
79+
80+
if (address) {
81+
supported.add(address.toLowerCase());
82+
}
83+
}
84+
}
85+
86+
return Promise.resolve(supported);
87+
};
88+
89+
/**
90+
* Returns per-category token identifiers that OneSec can bridge TO from the given source token.
91+
* - ICP source → { evm: Set<ERC20 address lowercased> } for the given EVM network IDs
92+
* - EVM source → { icp: Set<ICP ledger canister ID> }
93+
* - Unknown → undefined (no OneSec restriction applied)
94+
*/
95+
export const oneSecCompatibleDestinations = ({
96+
sourceToken,
97+
networkIds
98+
}: {
99+
sourceToken: AppToken;
100+
networkIds: NetworkId[];
101+
}): Partial<Record<'icp' | 'evm' | 'sol', Set<string>>> | undefined => {
102+
if (!ONESEC_SWAP_ENABLED) {
103+
return;
104+
}
105+
106+
if (isIcToken(sourceToken)) {
107+
const entry = ICP_LEDGER_TO_TOKEN[sourceToken.ledgerCanisterId];
108+
if (isNullish(entry)) {
109+
return;
110+
}
111+
112+
const addresses = new Set<string>();
113+
for (const networkId of networkIds) {
114+
const address = getEvmAddressForNetwork({ config: entry.config, networkId });
115+
if (nonNullish(address)) {
116+
addresses.add(address.toLowerCase());
117+
}
118+
}
119+
120+
return addresses.size > 0 ? { evm: addresses } : undefined;
121+
}
122+
123+
// EVM source: find OneSec token by matching ERC20 address on the source network
124+
const srcAddress = (sourceToken as Erc20Token).address;
125+
if (isNullish(srcAddress)) {
126+
return;
127+
}
128+
129+
for (const [, config] of DEFAULT_CONFIG.tokens) {
130+
const address = getEvmAddressForNetwork({ config, networkId: sourceToken.network.id });
131+
if (nonNullish(address) && address.toLowerCase() === srcAddress.toLowerCase()) {
132+
const ledger = config.ledgerMainnet ?? config.ledger;
133+
return nonNullish(ledger) ? { icp: new Set([ledger]) } : undefined;
134+
}
135+
}
136+
};

0 commit comments

Comments
 (0)