Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion app2/src/lib/stores/fee.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const createFeeStore = () => {
const applyRatioK = (a: BaseGasPrice): Writer.Writer<readonly string[][], BaseGasPrice> => {
const ratio = BigDecimal.round(
BigDecimal.unsafeDivide(BigDecimal.make(1n, 0), self.ratio),
{ scale: 6, mode: "half-even" },
{ scale: 6, mode: "from-zero" },
)
const result = pipe(
BigDecimal.multiply(a, ratio),
Expand Down
3 changes: 0 additions & 3 deletions app2/src/lib/transfer/shared/data/transfer-data.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,6 @@ export class TransferData {

version = $derived(pipe(
this.channel,
Option.tap((x) => {
return Option.some(x)
}),
Option.map(Struct.get("tags")),
Option.map(A.contains<"canonical" | "tokenorder-v2">("tokenorder-v2")),
Option.map(B.match({
Expand Down
170 changes: 119 additions & 51 deletions app2/src/lib/transfer/shared/services/filling/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import type {
TokenRawAmount,
UniversalChainId,
} from "@unionlabs/sdk/schema"
import { Effect, Match, Option, ParseResult, pipe } from "effect"
import { Effect, Either, flow, Match, Option, ParseResult, pipe } from "effect"
import * as A from "effect/Array"
import type { NoSuchElementException, UnknownException } from "effect/Cause"
import { constFalse, constTrue } from "effect/Function"
import * as S from "effect/Schema"
import { fromHex, isHex } from "viem"
import { GenericFlowError } from "../../errors"
import type { TransferArgs } from "./check-filling"

export type Intent = {
Expand Down Expand Up @@ -51,17 +53,35 @@ export const createContext = Effect.fn((
args: TransferArgs,
): Effect.Effect<
TransferContext,
NoSuchElementException | ParseResult.ParseError | UnknownException,
NoSuchElementException | ParseResult.ParseError | UnknownException | GenericFlowError,
never
> =>
Effect.gen(function*() {
console.debug("[createContext] args:", args)

const baseAmount = yield* parseBaseAmount(args.baseAmount).pipe(
Effect.mapError((cause) =>
new GenericFlowError({
message: "Could not parse base amount",
cause,
})
),
)

const quoteAmount = yield* parseBaseAmount(args.quoteAmount).pipe(
Effect.mapError((cause) =>
new GenericFlowError({
message: "Could not parse quote amount",
cause,
})
),
)

const sendOrder = yield* TokenOrder.make({
baseAmount: Option.getOrThrow(parseBaseAmount(args.baseAmount)),
baseAmount,
baseToken: args.baseToken,
quoteToken: args.quoteToken,
quoteAmount: Option.getOrThrow(parseBaseAmount(args.quoteAmount)),
quoteAmount,
destination: args.destinationChain,
receiver: args.receiver,
sender: args.sender,
Expand All @@ -78,43 +98,66 @@ export const createContext = Effect.fn((
})

// on destination chain tokens, find wrappings[] such that one exists where unwrapped_denom matches basetoken and unwrapped_chain and wrapped_chain universal ids match
const encodedFeeBaseToken = S.encodeSync(Token.AnyFromEncoded(args.sourceChain.rpc_type))(
const encodedFeeBaseToken = yield* pipe(
args.fee.baseToken,
S.encode(Token.AnyFromEncoded(args.sourceChain.rpc_type)),
Effect.mapError((cause) =>
new GenericFlowError({
message: "Could not base token",
cause,
})
),
)

const shouldIncludeFees = shouldChargeFees(args.fee, uiStore.edition, args.sourceChain)
const shouldIncludeFees = shouldChargeFees(
args.fee,
uiStore.edition,
args.sourceChain,
args.destinationChain,
)

const produceBatch = Effect.gen(function*() {
console.log({ shouldIncludeFees })
if (shouldIncludeFees) {
const feeQuoteToken = yield* maybeFeeQuoteToken.pipe(
Option.orElse(() =>
const feeQuoteToken = yield* Effect.if(args.baseToken.address === "au", {
onTrue: () => Effect.succeed(args.quoteToken),
onFalse: () =>
pipe(
tokensStore.getData(args.destinationChain.universal_chain_id),
Option.flatMap(
A.findFirst((token) =>
A.filter(token.wrapping, (x) =>
x.unwrapped_denom === encodedFeeBaseToken
&& x.unwrapped_chain.universal_chain_id === args.sourceChain.universal_chain_id
&& x.wrapped_chain.universal_chain_id
=== args.destinationChain.universal_chain_id)
.length
=== 1
),
maybeFeeQuoteToken,
Either.fromOption(() => "No fee quote token"),
Either.orElse(() =>
pipe(
tokensStore.getData(args.destinationChain.universal_chain_id),
Either.fromOption(() => "No matching token in token store"),
Either.flatMap(flow(
A.findFirst((token) =>
A.filter(token.wrapping, (x) =>
x.unwrapped_denom === encodedFeeBaseToken
&& x.unwrapped_chain.universal_chain_id
=== args.sourceChain.universal_chain_id
&& x.wrapped_chain.universal_chain_id
=== args.destinationChain.universal_chain_id)
.length
=== 1
),
Either.fromOption(() =>
`No quote token wrapping found for ${args.destinationChain.universal_chain_id} given ${args.fee.baseToken}`
),
)),
Either.map(x => x.denom),
Either.flatMap((raw) =>
S.decodeEither(Token.AnyFromEncoded(args.destinationChain.rpc_type))(raw)
),
)
),
Option.map(x => x.denom),
Option.flatMap((raw) =>
S.decodeOption(Token.AnyFromEncoded(args.destinationChain.rpc_type))(raw)
Effect.mapError((cause) =>
new GenericFlowError({
message: "Could not determine fee quote token",
cause,
})
),
)
),
Option.orElse(() => {
if (args.baseToken.address === "au") {
return Option.some(args.quoteToken)
}
console.error("Could not determine fee quote token.")
return Option.none()
}),
)
),
})

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

return Batch.make([sendOrder, feeOrder]).pipe(
return pipe(
Batch.make([sendOrder, feeOrder]),
Batch.optimize,
)
} else {
Expand All @@ -150,19 +194,17 @@ export const createContext = Effect.fn((
Option.some,
)

const ctx = yield* parseBaseAmount(args.baseAmount).pipe(
Option.flatMap((baseAmount) => {
const intents = createIntents(args, baseAmount)

return intents.length > 0
const ctx = yield* pipe(
createIntents(args, baseAmount),
(intents) =>
intents.length > 0
? Option.some({
intents,
allowances: Option.none(),
request: Option.none(),
message: Option.none(),
})
: Option.none()
}),
: Option.none(),
)

return {
Expand All @@ -173,7 +215,12 @@ export const createContext = Effect.fn((
)

const createIntents = (args: TransferArgs, baseAmount: TokenRawAmount): Intent[] => {
const shouldIncludeFees = shouldChargeFees(args.fee, uiStore.edition, args.sourceChain)
const shouldIncludeFees = shouldChargeFees(
args.fee,
uiStore.edition,
args.sourceChain,
args.destinationChain,
)
const baseIntent = createBaseIntent(args, baseAmount)

return Match.value(args.sourceChain.rpc_type).pipe(
Expand Down Expand Up @@ -240,16 +287,37 @@ const createBaseIntent = (
ucs03address: args.ucs03address,
})

// Fee strategy: BTC edition only charges fees when going FROM Babylon
const shouldChargeFees = (fee: FeeIntent, edition: string, sourceChain: Chain): boolean => {
if (fee.baseAmount === 0n) {
return false
}
if (sourceChain.testnet) {
return true
}
return sourceChain.universal_chain_id === "babylon.bbn-1"
}
const shouldChargeFees = (
fee: FeeIntent,
edition: string,
sourceChain: Chain,
destinationChain: Chain,
): boolean =>
pipe(
Match.value({
baseAmount: fee.baseAmount,
edition,
sourceChain,
destinationChain,
}),
Match.when(
{ baseAmount: 0n },
constFalse,
),
Match.when(
{ sourceChain: { testnet: true } },
constTrue,
),
Match.when(
{ sourceChain: { universal_chain_id: "babylon.bbn-1" } },
constTrue,
),
Match.whenOr(
{ destinationChain: { universal_chain_id: "ethereum.1" } },
constTrue,
),
Match.orElse(constTrue),
)

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