Skip to content

Commit 39e170e

Browse files
committed
feat(app2): save validation progress
1 parent d038084 commit 39e170e

File tree

6 files changed

+139
-53
lines changed

6 files changed

+139
-53
lines changed

app2/src/lib/components/Transfer/index.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ let buttonText = $derived(
117117
class="mt-2"
118118
variant="primary"
119119
onclick={transfer.submit}
120-
disabled={!isButtonEnabled}
120+
disabled={!isButtonEnabled || !transfer.validation.isValid}
121121
>
122122
{buttonText}
123123
</Button>

app2/src/lib/components/Transfer/transfer.svelte.ts

+40-47
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect, Either, Option, Schema } from "effect"
1+
import { Effect, Option } from "effect"
22
import { RawTransferSvelte } from "./raw-transfer.svelte.ts"
33
import type { QuoteData, Token, WethTokenData } from "$lib/schema/token.ts"
44
import { tokensStore } from "$lib/stores/tokens.svelte.ts"
@@ -26,6 +26,7 @@ import {
2626
hasFailedExit as hasAptosFailedExit,
2727
isComplete as isAptosComplete,
2828
nextState as aptosNextState,
29+
TransferSubmitState as AptosTransferSubmitState,
2930
TransferSubmission as AptosTransferSubmission,
3031
TransferReceiptState as AptosTransferReceiptState
3132
} from "$lib/services/transfer-ucs03-aptos"
@@ -34,13 +35,13 @@ import { type Address, fromHex, type Hex } from "viem"
3435
import { channels } from "$lib/stores/channels.svelte.ts"
3536
import { getChannelInfoSafe } from "$lib/services/transfer-ucs03-evm/channel.ts"
3637
import type { Channel } from "$lib/schema/channel.ts"
37-
import { TransferSchema } from "$lib/schema/transfer-args.ts"
3838
import { getQuoteToken as getQuoteTokenEffect } from "$lib/services/shared"
3939
import { getWethQuoteToken as getWethQuoteTokenEffect } from "$lib/services/shared"
4040
import { cosmosStore } from "$lib/wallet/cosmos"
4141
import { getParsedAmountSafe } from "$lib/services/shared"
4242
import { getDerivedReceiverSafe } from "$lib/services/shared"
4343
import { sortedBalancesStore } from "$lib/stores/sorted-balances.svelte.ts"
44+
import { validateTransfer, type ValidationResult } from "$lib/components/Transfer/validation.ts"
4445

4546
export interface TransferState {
4647
readonly _tag: string
@@ -300,47 +301,55 @@ export class Transfer {
300301
const ucs03addressValue = Option.getOrNull(this.ucs03address)
301302
const wethQuoteTokenValue = Option.getOrNull(this.wethQuoteToken)
302303

304+
const maybeQuoteToken =
305+
quoteTokenValue &&
306+
(quoteTokenValue.type === "UNWRAPPED" || quoteTokenValue.type === "NEW_WRAPPED")
307+
? quoteTokenValue.quote_token
308+
: undefined
309+
310+
const maybeWethQuoteToken =
311+
wethQuoteTokenValue && "wethQuoteToken" in wethQuoteTokenValue
312+
? (wethQuoteTokenValue as { wethQuoteToken: string }).wethQuoteToken
313+
: undefined
314+
303315
return {
304-
sourceChain: sourceChainValue
305-
? sourceChainValue.rpc_type === "evm"
306-
? sourceChainValue.toViemChain()
307-
: sourceChainValue
308-
: null,
316+
sourceChain: sourceChainValue,
309317
sourceRpcType: sourceChainValue?.rpc_type,
310318
destinationRpcType: destinationChainValue?.rpc_type,
311319
sourceChannelId: channelValue?.source_channel_id,
312320
ucs03address: ucs03addressValue,
313321
baseToken: baseTokenValue?.denom,
314322
baseAmount: parsedAmountValue,
315-
quoteToken: quoteTokenValue?.quote_token,
323+
quoteToken: maybeQuoteToken,
316324
quoteAmount: parsedAmountValue,
317325
receiver: derivedReceiverValue,
318-
timeoutHeight: 0n,
326+
timeoutHeight: "0n",
319327
timeoutTimestamp: "0x000000000000000000000000000000000000000000000000fffffffffffffffa",
320-
wethQuoteToken: wethQuoteTokenValue?.wethQuoteToken
328+
wethQuoteToken: maybeWethQuoteToken
321329
}
322330
})
323331

324-
transferResult = $derived.by(() => {
325-
const validationEffect = Schema.decode(TransferSchema)(this.args)
326-
const result = Effect.runSync(Effect.either(validationEffect))
327-
return Either.isRight(result)
328-
? { isValid: true, args: result.right }
329-
: { isValid: false, args: this.args }
330-
})
332+
validation = $derived.by<ValidationResult>(() => validateTransfer(this.args))
331333

332-
isValid = $derived(this.transferResult.isValid)
334+
isValid = $derived(this.validation.isValid)
333335

334336
submit = async () => {
337+
const validation = this.validation
338+
if (!validation.isValid) {
339+
console.warn("Validation failed, errors:", validation.messages)
340+
return
341+
}
342+
343+
const typedArgs = validation.value
344+
335345
if (Option.isNone(chains.data) || Option.isNone(this.sourceChain)) return
336-
console.log(this.transferResult.args)
346+
console.info("Validated args:", typedArgs)
337347

338348
const sourceChainValue = this.sourceChain.value
339349

340350
if (sourceChainValue.rpc_type === "evm") {
341351
let evmState: EvmTransferSubmission
342352
if (this.state._tag === "EVM") {
343-
// If failed, reset the failed step to InProgress
344353
if (hasEvmFailedExit(this.state.state)) {
345354
switch (this.state.state._tag) {
346355
case "SwitchChain":
@@ -378,17 +387,12 @@ export class Transfer {
378387
evmState = EvmTransferSubmission.Filling()
379388
}
380389

381-
const newState = await evmNextState(evmState, this.transferResult.args, sourceChainValue)
390+
const newState = await evmNextState(evmState, typedArgs, sourceChainValue)
382391
this._stateOverride = newState !== null ? TransferState.EVM(newState) : TransferState.Empty()
383392

384-
console.info("evmState: ", evmState)
385393
let currentEvmState = newState
386394
while (currentEvmState !== null && !hasEvmFailedExit(currentEvmState)) {
387-
const nextEvmState = await evmNextState(
388-
currentEvmState,
389-
this.transferResult.args,
390-
sourceChainValue
391-
)
395+
const nextEvmState = await evmNextState(currentEvmState, typedArgs, sourceChainValue)
392396
this._stateOverride =
393397
nextEvmState !== null ? TransferState.EVM(nextEvmState) : TransferState.Empty()
394398

@@ -398,7 +402,6 @@ export class Transfer {
398402
} else if (sourceChainValue.rpc_type === "cosmos") {
399403
let cosmosState: CosmosTransferSubmission
400404
if (this.state._tag === "Cosmos") {
401-
// If failed, reset the failed step to InProgress
402405
if (hasCosmosFailedExit(this.state.state)) {
403406
switch (this.state.state._tag) {
404407
case "SwitchChain":
@@ -428,7 +431,7 @@ export class Transfer {
428431

429432
const newState = await cosmosNextState(
430433
cosmosState,
431-
this.transferResult.args,
434+
typedArgs,
432435
sourceChainValue,
433436
cosmosStore.connectedWallet
434437
)
@@ -439,7 +442,7 @@ export class Transfer {
439442
while (currentCosmosState !== null && !hasCosmosFailedExit(currentCosmosState)) {
440443
const nextCosmosState = await cosmosNextState(
441444
currentCosmosState,
442-
this.transferResult.args,
445+
typedArgs,
443446
sourceChainValue,
444447
cosmosStore.connectedWallet
445448
)
@@ -450,12 +453,8 @@ export class Transfer {
450453
if (currentCosmosState !== null && isCosmosComplete(currentCosmosState)) break
451454
}
452455
} else if (sourceChainValue.rpc_type === "aptos") {
453-
console.info("sourceChain is aptos")
454-
console.info("this.state._tag is: ", this.state._tag)
455456
let aptosState: AptosTransferSubmission
456457
if (this.state._tag === "Aptos") {
457-
console.info("state._tag is aptos")
458-
// If failed, reset the failed step to InProgress
459458
if (hasAptosFailedExit(this.state.state)) {
460459
switch (this.state.state._tag) {
461460
case "SwitchChain":
@@ -483,24 +482,18 @@ export class Transfer {
483482
aptosState = AptosTransferSubmission.Filling()
484483
}
485484

486-
console.info("aptosState: ", aptosState)
487-
488-
const newState = await aptosNextState(aptosState, this.transferResult.args, sourceChainValue)
485+
const newState = await aptosNextState(aptosState, typedArgs, sourceChainValue)
489486
this._stateOverride =
490487
newState !== null ? TransferState.Aptos(newState) : TransferState.Empty()
491488

492-
let currentaptosState = newState
493-
while (currentaptosState !== null && !hasAptosFailedExit(currentaptosState)) {
494-
const nextaptosState = await aptosNextState(
495-
currentaptosState,
496-
this.transferResult.args,
497-
sourceChainValue
498-
)
489+
let currentAptosState = newState
490+
while (currentAptosState !== null && !hasAptosFailedExit(currentAptosState)) {
491+
const nextAptosState = await aptosNextState(currentAptosState, typedArgs, sourceChainValue)
499492
this._stateOverride =
500-
nextaptosState !== null ? TransferState.Aptos(nextaptosState) : TransferState.Empty()
493+
nextAptosState !== null ? TransferState.Aptos(nextAptosState) : TransferState.Empty()
501494

502-
currentaptosState = nextaptosState
503-
if (currentaptosState !== null && isAptosComplete(currentaptosState)) break
495+
currentAptosState = nextAptosState
496+
if (currentAptosState !== null && isAptosComplete(currentAptosState)) break
504497
}
505498
}
506499
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Effect, Either, ParseResult, Schema } from "effect"
2+
import {
3+
type AptosTransfer,
4+
type CosmosTransfer,
5+
type EVMTransfer,
6+
TransferSchema
7+
} from "$lib/schema/transfer-args.ts"
8+
9+
export type ValidationSuccess = {
10+
isValid: true
11+
value: EVMTransfer | CosmosTransfer | AptosTransfer
12+
errors: []
13+
messages: []
14+
fieldErrors: Record<string, Array<string>> // empty if valid
15+
}
16+
17+
export type ValidationFailure = {
18+
isValid: false
19+
value: undefined
20+
errors: unknown
21+
messages: Array<string>
22+
fieldErrors: Record<string, Array<string>>
23+
}
24+
25+
export type ValidationResult = ValidationSuccess | ValidationFailure
26+
27+
const decodeAll = Schema.decodeUnknown(TransferSchema, { errors: "all" })
28+
29+
export function validateTransfer(args: unknown): ValidationResult {
30+
const decodeEither = Effect.runSync(Effect.either(decodeAll(args)))
31+
32+
if (Either.isRight(decodeEither)) {
33+
return {
34+
isValid: true,
35+
value: decodeEither.right,
36+
errors: [],
37+
messages: [],
38+
fieldErrors: {}
39+
}
40+
}
41+
42+
const parseError = decodeEither.left
43+
const arrayOutput = ParseResult.ArrayFormatter.formatErrorSync(parseError)
44+
45+
const messages = arrayOutput.map(errObj => {
46+
return `Path: [${errObj.path.join(", ")}], message: ${errObj.message}`
47+
})
48+
49+
const fieldErrors: Record<string, Array<string>> = {}
50+
51+
for (const { path, message } of arrayOutput) {
52+
const [field] = path
53+
54+
if (typeof field === "string") {
55+
if (!fieldErrors[field]) {
56+
fieldErrors[field] = []
57+
}
58+
fieldErrors[field].push(message)
59+
} else {
60+
if (!fieldErrors["_general"]) {
61+
fieldErrors["_general"] = []
62+
}
63+
fieldErrors["_general"].push(message)
64+
}
65+
}
66+
67+
return {
68+
isValid: false,
69+
value: undefined,
70+
errors: parseError,
71+
messages,
72+
fieldErrors
73+
}
74+
}

app2/src/lib/schema/token.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const EVMWethToken = AddressEvmCanonical.pipe(
7272

7373
export const QuoteData = Schema.Union(
7474
Schema.Struct({
75-
quote_token: Schema.String,
75+
quote_token: Hex,
7676
type: Schema.Literal("UNWRAPPED", "NEW_WRAPPED")
7777
}),
7878
Schema.Struct({
@@ -88,7 +88,7 @@ export const QuoteData = Schema.Union(
8888
)
8989

9090
export const WethTokenData = Schema.Union(
91-
Schema.Struct({ wethQuoteToken: Schema.String }),
91+
Schema.Struct({ wethQuoteToken: Hex }),
9292
Schema.Struct({ type: Schema.Literal("NO_WETH_QUOTE") }),
9393
Schema.Struct({ type: Schema.Literal("WETH_LOADING") }),
9494
Schema.Struct({ type: Schema.Literal("WETH_MISSING_ARGUMENTS") }),

app2/src/lib/schema/transfer-args.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ const BaseTransferFields = {
3232

3333
const EVMTransferSchema = Schema.Struct({
3434
...BaseTransferFields,
35-
sourceRpcType: Schema.Literal("evm"),
35+
sourceRpcType: Schema.Literal("evm").annotations({
36+
message: () => "sourceRpcType must be 'evm'"
37+
}),
3638
wethQuoteToken: EVMWethToken,
3739
receiver: Schema.String.pipe(
3840
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
@@ -43,7 +45,9 @@ export class EVMTransfer extends Schema.Class<EVMTransfer>("EVMTransfer")(EVMTra
4345

4446
const CosmosTransferSchema = Schema.Struct({
4547
...BaseTransferFields,
46-
sourceRpcType: Schema.Literal("cosmos"),
48+
sourceRpcType: Schema.Literal("cosmos").annotations({
49+
message: () => "sourceRpcType must be 'cosmos'"
50+
}),
4751
receiver: Schema.String.pipe(
4852
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
4953
)
@@ -55,7 +59,9 @@ export class CosmosTransfer extends Schema.Class<CosmosTransfer>("CosmosTransfer
5559

5660
const AptosTransferSchema = Schema.Struct({
5761
...BaseTransferFields,
58-
sourceRpcType: Schema.Literal("aptos"),
62+
sourceRpcType: Schema.Literal("aptos").annotations({
63+
message: () => "sourceRpcType must be 'aptos'"
64+
}),
5965
receiver: Schema.String.pipe(
6066
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
6167
)

app2/src/routes/transfer/+layout.svelte

+13
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,17 @@ $effect(() => {
4747

4848
{@render children()}
4949

50+
{#if transfer.validation.isValid}
51+
<p class="text-sm">Everything looks good!</p>
52+
{:else}
53+
<p>Transfer validation errors:</p>
54+
<ul class="text-xs">
55+
{#each transfer.validation.messages ?? [] as msg}
56+
<li>{msg}</li>
57+
{/each}
58+
</ul>
59+
{/if}
60+
61+
62+
5063

0 commit comments

Comments
 (0)