Skip to content

Commit 86df8e5

Browse files
committed
feat(app): add mainnet fee handling and errors
Signed-off-by: Eric Hegnes <eric@hegnes.com>
1 parent 876e02a commit 86df8e5

File tree

3 files changed

+120
-55
lines changed

3 files changed

+120
-55
lines changed

app2/src/lib/stores/fee.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ const createFeeStore = () => {
281281
const applyRatioK = (a: BaseGasPrice): Writer.Writer<readonly string[][], BaseGasPrice> => {
282282
const ratio = BigDecimal.round(
283283
BigDecimal.unsafeDivide(BigDecimal.make(1n, 0), self.ratio),
284-
{ scale: 6, mode: "half-even" },
284+
{ scale: 6, mode: "from-zero" },
285285
)
286286
const result = pipe(
287287
BigDecimal.multiply(a, ratio),

app2/src/lib/transfer/shared/data/transfer-data.svelte.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,6 @@ export class TransferData {
310310

311311
version = $derived(pipe(
312312
this.channel,
313-
Option.tap((x) => {
314-
return Option.some(x)
315-
}),
316313
Option.map(Struct.get("tags")),
317314
Option.map(A.contains<"canonical" | "tokenorder-v2">("tokenorder-v2")),
318315
Option.map(B.match({

app2/src/lib/transfer/shared/services/filling/create-context.ts

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import type {
1010
TokenRawAmount,
1111
UniversalChainId,
1212
} from "@unionlabs/sdk/schema"
13-
import { Effect, Match, Option, ParseResult, pipe } from "effect"
13+
import { Effect, Either, flow, Match, Option, ParseResult, pipe } from "effect"
1414
import * as A from "effect/Array"
1515
import type { NoSuchElementException, UnknownException } from "effect/Cause"
16+
import { constFalse, constTrue } from "effect/Function"
1617
import * as S from "effect/Schema"
1718
import { fromHex, isHex } from "viem"
19+
import { GenericFlowError } from "../../errors"
1820
import type { TransferArgs } from "./check-filling"
1921

2022
export type Intent = {
@@ -51,17 +53,35 @@ export const createContext = Effect.fn((
5153
args: TransferArgs,
5254
): Effect.Effect<
5355
TransferContext,
54-
NoSuchElementException | ParseResult.ParseError | UnknownException,
56+
NoSuchElementException | ParseResult.ParseError | UnknownException | GenericFlowError,
5557
never
5658
> =>
5759
Effect.gen(function*() {
5860
console.debug("[createContext] args:", args)
5961

62+
const baseAmount = yield* parseBaseAmount(args.baseAmount).pipe(
63+
Effect.mapError((cause) =>
64+
new GenericFlowError({
65+
message: "Could not parse base amount",
66+
cause,
67+
})
68+
),
69+
)
70+
71+
const quoteAmount = yield* parseBaseAmount(args.quoteAmount).pipe(
72+
Effect.mapError((cause) =>
73+
new GenericFlowError({
74+
message: "Could not parse quote amount",
75+
cause,
76+
})
77+
),
78+
)
79+
6080
const sendOrder = yield* TokenOrder.make({
61-
baseAmount: Option.getOrThrow(parseBaseAmount(args.baseAmount)),
81+
baseAmount,
6282
baseToken: args.baseToken,
6383
quoteToken: args.quoteToken,
64-
quoteAmount: Option.getOrThrow(parseBaseAmount(args.quoteAmount)),
84+
quoteAmount,
6585
destination: args.destinationChain,
6686
receiver: args.receiver,
6787
sender: args.sender,
@@ -78,43 +98,66 @@ export const createContext = Effect.fn((
7898
})
7999

80100
// on destination chain tokens, find wrappings[] such that one exists where unwrapped_denom matches basetoken and unwrapped_chain and wrapped_chain universal ids match
81-
const encodedFeeBaseToken = S.encodeSync(Token.AnyFromEncoded(args.sourceChain.rpc_type))(
101+
const encodedFeeBaseToken = yield* pipe(
82102
args.fee.baseToken,
103+
S.encode(Token.AnyFromEncoded(args.sourceChain.rpc_type)),
104+
Effect.mapError((cause) =>
105+
new GenericFlowError({
106+
message: "Could not base token",
107+
cause,
108+
})
109+
),
83110
)
84111

85-
const shouldIncludeFees = shouldChargeFees(args.fee, uiStore.edition, args.sourceChain)
112+
const shouldIncludeFees = shouldChargeFees(
113+
args.fee,
114+
uiStore.edition,
115+
args.sourceChain,
116+
args.destinationChain,
117+
)
86118

87119
const produceBatch = Effect.gen(function*() {
120+
console.log({ shouldIncludeFees })
88121
if (shouldIncludeFees) {
89-
const feeQuoteToken = yield* maybeFeeQuoteToken.pipe(
90-
Option.orElse(() =>
122+
const feeQuoteToken = yield* Effect.if(args.baseToken.address === "au", {
123+
onTrue: () => Effect.succeed(args.quoteToken),
124+
onFalse: () =>
91125
pipe(
92-
tokensStore.getData(args.destinationChain.universal_chain_id),
93-
Option.flatMap(
94-
A.findFirst((token) =>
95-
A.filter(token.wrapping, (x) =>
96-
x.unwrapped_denom === encodedFeeBaseToken
97-
&& x.unwrapped_chain.universal_chain_id === args.sourceChain.universal_chain_id
98-
&& x.wrapped_chain.universal_chain_id
99-
=== args.destinationChain.universal_chain_id)
100-
.length
101-
=== 1
102-
),
126+
maybeFeeQuoteToken,
127+
Either.fromOption(() => "No fee quote token"),
128+
Either.orElse(() =>
129+
pipe(
130+
tokensStore.getData(args.destinationChain.universal_chain_id),
131+
Either.fromOption(() => "No matching token in token store"),
132+
Either.flatMap(flow(
133+
A.findFirst((token) =>
134+
A.filter(token.wrapping, (x) =>
135+
x.unwrapped_denom === encodedFeeBaseToken
136+
&& x.unwrapped_chain.universal_chain_id
137+
=== args.sourceChain.universal_chain_id
138+
&& x.wrapped_chain.universal_chain_id
139+
=== args.destinationChain.universal_chain_id)
140+
.length
141+
=== 1
142+
),
143+
Either.fromOption(() =>
144+
`No quote token wrapping found for ${args.destinationChain.universal_chain_id} given ${args.fee.baseToken}`
145+
),
146+
)),
147+
Either.map(x => x.denom),
148+
Either.flatMap((raw) =>
149+
S.decodeEither(Token.AnyFromEncoded(args.destinationChain.rpc_type))(raw)
150+
),
151+
)
103152
),
104-
Option.map(x => x.denom),
105-
Option.flatMap((raw) =>
106-
S.decodeOption(Token.AnyFromEncoded(args.destinationChain.rpc_type))(raw)
153+
Effect.mapError((cause) =>
154+
new GenericFlowError({
155+
message: "Could not determine fee quote token",
156+
cause,
157+
})
107158
),
108-
)
109-
),
110-
Option.orElse(() => {
111-
if (args.baseToken.address === "au") {
112-
return Option.some(args.quoteToken)
113-
}
114-
console.error("Could not determine fee quote token.")
115-
return Option.none()
116-
}),
117-
)
159+
),
160+
})
118161

119162
const feeOrder = yield* TokenOrder.make({
120163
baseAmount: args.fee.baseAmount,
@@ -130,7 +173,8 @@ export const createContext = Effect.fn((
130173
version: args.version,
131174
})
132175

133-
return Batch.make([sendOrder, feeOrder]).pipe(
176+
return pipe(
177+
Batch.make([sendOrder, feeOrder]),
134178
Batch.optimize,
135179
)
136180
} else {
@@ -150,19 +194,17 @@ export const createContext = Effect.fn((
150194
Option.some,
151195
)
152196

153-
const ctx = yield* parseBaseAmount(args.baseAmount).pipe(
154-
Option.flatMap((baseAmount) => {
155-
const intents = createIntents(args, baseAmount)
156-
157-
return intents.length > 0
197+
const ctx = yield* pipe(
198+
createIntents(args, baseAmount),
199+
(intents) =>
200+
intents.length > 0
158201
? Option.some({
159202
intents,
160203
allowances: Option.none(),
161204
request: Option.none(),
162205
message: Option.none(),
163206
})
164-
: Option.none()
165-
}),
207+
: Option.none(),
166208
)
167209

168210
return {
@@ -173,7 +215,12 @@ export const createContext = Effect.fn((
173215
)
174216

175217
const createIntents = (args: TransferArgs, baseAmount: TokenRawAmount): Intent[] => {
176-
const shouldIncludeFees = shouldChargeFees(args.fee, uiStore.edition, args.sourceChain)
218+
const shouldIncludeFees = shouldChargeFees(
219+
args.fee,
220+
uiStore.edition,
221+
args.sourceChain,
222+
args.destinationChain,
223+
)
177224
const baseIntent = createBaseIntent(args, baseAmount)
178225

179226
return Match.value(args.sourceChain.rpc_type).pipe(
@@ -240,16 +287,37 @@ const createBaseIntent = (
240287
ucs03address: args.ucs03address,
241288
})
242289

243-
// Fee strategy: BTC edition only charges fees when going FROM Babylon
244-
const shouldChargeFees = (fee: FeeIntent, edition: string, sourceChain: Chain): boolean => {
245-
if (fee.baseAmount === 0n) {
246-
return false
247-
}
248-
if (sourceChain.testnet) {
249-
return true
250-
}
251-
return sourceChain.universal_chain_id === "babylon.bbn-1"
252-
}
290+
const shouldChargeFees = (
291+
fee: FeeIntent,
292+
edition: string,
293+
sourceChain: Chain,
294+
destinationChain: Chain,
295+
): boolean =>
296+
pipe(
297+
Match.value({
298+
baseAmount: fee.baseAmount,
299+
edition,
300+
sourceChain,
301+
destinationChain,
302+
}),
303+
Match.when(
304+
{ baseAmount: 0n },
305+
constFalse,
306+
),
307+
Match.when(
308+
{ sourceChain: { testnet: true } },
309+
constTrue,
310+
),
311+
Match.when(
312+
{ sourceChain: { universal_chain_id: "babylon.bbn-1" } },
313+
constTrue,
314+
),
315+
Match.whenOr(
316+
{ destinationChain: { universal_chain_id: "ethereum.1" } },
317+
constTrue,
318+
),
319+
Match.orElse(constTrue),
320+
)
253321

254322
const normalizeToken = (token: string | `0x${string}`, rpcType: string): string => {
255323
return rpcType === "cosmos" && isHex(token) ? fromHex(token, "string") : token

0 commit comments

Comments
 (0)