Skip to content

Commit 1cb9dac

Browse files
committed
fix(app): fix bbn 1 fee and refactor
1 parent d239579 commit 1cb9dac

File tree

1 file changed

+91
-135
lines changed

1 file changed

+91
-135
lines changed
Lines changed: 91 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { uiStore } from "$lib/stores/ui.svelte.ts"
12
import { GAS_DENOMS } from "@unionlabs/sdk/constants/gas-denoms.ts"
23
import type {
34
AddressCanonicalBytes,
@@ -50,167 +51,122 @@ export type TransferContext = {
5051
export const createContext = (args: TransferArgs): Option.Option<TransferContext> => {
5152
console.debug("[createContext] args:", args)
5253

53-
let baseAmount: TokenRawAmount
54-
try {
55-
baseAmount = BigInt(args.baseAmount) as TokenRawAmount
56-
} catch (err) {
57-
console.warn("[createContext] baseAmount parse failed", err)
58-
return Option.none()
59-
}
54+
return parseBaseAmount(args.baseAmount).pipe(
55+
Option.flatMap(baseAmount => {
56+
const intents = createIntents(args, baseAmount)
57+
58+
return intents.length > 0
59+
? Option.some({
60+
intents,
61+
native: calculateNativeValue(intents, args),
62+
allowances: Option.none(),
63+
instruction: Option.none(),
64+
message: Option.none(),
65+
})
66+
: Option.none()
67+
}),
68+
)
69+
}
70+
71+
const createBaseIntent = (
72+
args: TransferArgs,
73+
baseAmount: TokenRawAmount,
74+
): Omit<Intent, "baseToken"> => ({
75+
sender: args.sender,
76+
receiver: args.receiver,
77+
baseAmount,
78+
quoteAmount: baseAmount,
79+
decimals: args.decimals,
80+
sourceChain: args.sourceChain,
81+
sourceChainId: args.sourceChain.universal_chain_id,
82+
sourceChannelId: args.channel.source_channel_id,
83+
destinationChain: args.destinationChain,
84+
channel: args.channel,
85+
ucs03address: args.ucs03address,
86+
})
87+
88+
const createIntents = (args: TransferArgs, baseAmount: TokenRawAmount): Intent[] => {
89+
const shouldIncludeFees = shouldChargeFees(uiStore.edition, args.sourceChain)
90+
const baseIntent = createBaseIntent(args, baseAmount)
6091

6192
return Match.value(args.sourceChain.rpc_type).pipe(
6293
Match.when("evm", () => {
6394
const intent: Intent = {
64-
sender: args.sender,
65-
receiver: args.receiver,
95+
...baseIntent,
6696
baseToken: args.baseToken,
67-
baseAmount,
68-
quoteAmount: baseAmount,
69-
decimals: args.decimals,
70-
sourceChain: args.sourceChain,
71-
sourceChainId: args.sourceChain.universal_chain_id,
72-
sourceChannelId: args.channel.source_channel_id,
73-
destinationChain: args.destinationChain,
74-
channel: args.channel,
75-
ucs03address: args.ucs03address,
7697
}
7798

7899
const feeIntent: Intent = {
79-
sender: args.sender,
80-
receiver: args.receiver,
100+
...baseIntent,
81101
baseToken: args.fee.baseToken,
82102
baseAmount: args.fee.baseAmount,
83103
quoteAmount: args.fee.quoteAmount,
84104
decimals: args.fee.decimals,
85-
sourceChain: args.sourceChain,
86-
sourceChainId: args.sourceChain.universal_chain_id,
87-
sourceChannelId: args.channel.source_channel_id,
88-
destinationChain: args.destinationChain,
89-
channel: args.channel,
90-
ucs03address: args.ucs03address,
91-
}
92-
93-
// Calculate native value for EVM
94-
const calculateNativeValue = () => {
95-
const chainGasDenom = GAS_DENOMS[args.sourceChain.universal_chain_id]
96-
if (!chainGasDenom) {
97-
return Option.none()
98-
}
99-
100-
let totalAmount = 0n
101-
102-
// Check if intent uses native token
103-
if (intent.baseToken === chainGasDenom.address) {
104-
totalAmount += intent.baseAmount
105-
}
106-
107-
// Check if fee intent uses native token
108-
if (feeIntent.baseToken === chainGasDenom.address) {
109-
totalAmount += feeIntent.baseAmount
110-
}
111-
112-
if (totalAmount > 0n) {
113-
return Option.some({
114-
baseToken: args.fee.baseToken, // Always use fee baseToken
115-
amount: totalAmount as TokenRawAmount,
116-
})
117-
}
118-
119-
return Option.none()
120105
}
121106

122-
return Option.some({
123-
intents: [intent, feeIntent],
124-
native: calculateNativeValue(),
125-
allowances: Option.none(),
126-
instruction: Option.none(),
127-
message: Option.none(),
128-
})
107+
return shouldIncludeFees ? [intent, feeIntent] : [intent]
129108
}),
130109
Match.when("cosmos", () => {
131-
const baseToken = isHex(args.baseToken) ? fromHex(args.baseToken, "string") : args.baseToken
132-
133110
const intent: Intent = {
134-
sender: args.sender,
135-
// XXX: guarantee lowercase as part of schema transform
136-
receiver: args.receiver.toLowerCase() as typeof args.receiver,
137-
baseToken: baseToken,
138-
baseAmount: baseAmount,
139-
quoteAmount: baseAmount,
140-
decimals: args.decimals,
141-
sourceChain: args.sourceChain,
142-
sourceChainId: args.sourceChain.universal_chain_id,
143-
sourceChannelId: args.channel.source_channel_id,
144-
destinationChain: args.destinationChain,
145-
channel: args.channel,
146-
ucs03address: args.ucs03address,
111+
...baseIntent,
112+
baseToken: normalizeToken(args.baseToken, "cosmos"),
147113
}
148114

149115
const feeIntent: Intent = {
150-
sender: args.sender.toLowerCase() as typeof args.sender,
151-
receiver: args.receiver.toLowerCase() as typeof args.receiver,
152-
baseToken: isHex(args.fee.baseToken)
153-
? fromHex(args.fee.baseToken, "string")
154-
: args.fee.baseToken,
116+
...baseIntent,
117+
baseToken: normalizeToken(args.fee.baseToken, "cosmos"),
155118
baseAmount: args.fee.baseAmount,
156119
quoteAmount: args.fee.quoteAmount,
157120
decimals: args.fee.decimals,
158-
sourceChain: args.sourceChain,
159-
sourceChainId: args.sourceChain.universal_chain_id,
160-
sourceChannelId: args.channel.source_channel_id,
161-
destinationChain: args.destinationChain,
162-
channel: args.channel,
163-
ucs03address: args.ucs03address,
164121
}
165122

166-
// Calculate native value for Cosmos
167-
const calculateNativeValue = () => {
168-
const chainGasDenom = GAS_DENOMS[args.sourceChain.universal_chain_id]
169-
if (!chainGasDenom) {
170-
return Option.none()
171-
}
172-
173-
let totalAmount = 0n
174-
175-
// Convert hex format to string for comparison
176-
const nativeTokenString = fromHex(chainGasDenom.address, "string")
177-
178-
// Check if intent uses native token
179-
if (intent.baseToken === nativeTokenString) {
180-
totalAmount += intent.baseAmount
181-
}
182-
183-
// Check if fee intent uses native token
184-
if (feeIntent.baseToken === nativeTokenString) {
185-
totalAmount += feeIntent.baseAmount
186-
}
187-
188-
if (totalAmount > 0n) {
189-
// For Cosmos, ensure fee baseToken is in string format (not hex)
190-
const feeBaseToken = isHex(args.fee.baseToken)
191-
? fromHex(args.fee.baseToken, "string")
192-
: args.fee.baseToken
193-
194-
return Option.some({
195-
baseToken: feeBaseToken,
196-
amount: totalAmount as TokenRawAmount,
197-
})
198-
}
199-
200-
return Option.none()
201-
}
202-
203-
return Option.some({
204-
intents: [intent, feeIntent],
205-
native: calculateNativeValue(),
206-
allowances: Option.none(),
207-
instruction: Option.none(),
208-
message: Option.none(),
209-
})
123+
return shouldIncludeFees ? [intent, feeIntent] : [intent]
210124
}),
211-
Match.orElse(() => {
212-
console.warn("[createContext] Unknown chain rpc_type", args.sourceChain.rpc_type)
213-
return Option.none()
125+
Match.orElse(() => []),
126+
)
127+
}
128+
129+
// Fee strategy: BTC edition only charges fees when going FROM Babylon to cosmos
130+
const shouldChargeFees = (edition: string, sourceChain: Chain): boolean => {
131+
return Match.value(edition).pipe(
132+
Match.when("btc", () => sourceChain.universal_chain_id === "babylon.bbn-1"),
133+
Match.orElse(() => true),
134+
)
135+
}
136+
137+
const normalizeToken = (token: string | `0x${string}`, rpcType: string): string => {
138+
return rpcType === "cosmos" && isHex(token) ? fromHex(token, "string") : token
139+
}
140+
141+
const parseBaseAmount = (amount: string): Option.Option<TokenRawAmount> => {
142+
return Option.fromNullable(amount)
143+
.pipe(
144+
Option.filter(str => str.trim() !== ""),
145+
Option.filter(str => /^\d+$/.test(str.trim())),
146+
Option.map(str => BigInt(str.trim()) as TokenRawAmount),
147+
)
148+
}
149+
150+
const calculateNativeValue = (
151+
intents: Intent[],
152+
args: TransferArgs,
153+
): Option.Option<{ baseToken: TokenRawDenom | string; amount: TokenRawAmount }> => {
154+
return Option.fromNullable(GAS_DENOMS[args.sourceChain.universal_chain_id]).pipe(
155+
Option.flatMap(chainGasDenom => {
156+
const nativeToken = normalizeToken(chainGasDenom.address, args.sourceChain.rpc_type)
157+
const nativeIntents = intents.filter(intent =>
158+
normalizeToken(intent.baseToken, args.sourceChain.rpc_type) === nativeToken
159+
)
160+
161+
const totalAmount = nativeIntents.reduce((sum, intent) => sum + intent.baseAmount, 0n)
162+
const preferredBaseToken = nativeIntents.at(-1)?.baseToken || nativeToken
163+
164+
return totalAmount > 0n
165+
? Option.some({
166+
baseToken: preferredBaseToken,
167+
amount: totalAmount as TokenRawAmount,
168+
})
169+
: Option.none()
214170
}),
215171
)
216172
}

0 commit comments

Comments
 (0)