diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..43f3a6d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(bun run format:*)", + "Bash(bun run lint:*)", + "Bash(bunx eslint:*)", + "Bash(bun run check:*)", + "Bash(bun run:*)", + "Bash(bun install:*)" + ] + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e4956f..95a45ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,10 @@ jobs: run: bun install --frozen-lockfile - name: Run checks run: bun run check + env: + PRIVATE_POLYMER_MAINNET_ZONE_API_KEY: ${{ secrets.PRIVATE_POLYMER_MAINNET_ZONE_API_KEY }} + PRIVATE_POLYMER_TESTNET_ZONE_API_KEY: ${{ secrets.PRIVATE_POLYMER_TESTNET_ZONE_API_KEY }} + PUBLIC_WALLET_CONNECT_PROJECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_PROJECT_ID }} - name: Run unit tests run: bun run test:unit - name: Upload coverage artifact diff --git a/bun.lock b/bun.lock index 7c7ab35..abf1db4 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.6", "@electric-sql/pglite": "^0.3.15", - "@lifi/intent": "0.0.3-alpha.1", + "@lifi/intent": "0.0.4", "@metamask/sdk": "^0.34.0", "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", @@ -231,7 +231,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], - "@lifi/intent": ["@lifi/intent@0.0.3-alpha.1", "", { "dependencies": { "ky": "^1.12.0", "viem": "~2.45.1" } }, "sha512-dzEcS8U5buW7nLpkMmC0kj5R7EQC7l8l1mBd9IWr6R5/vSnSF3kO9zVaNQOmrbezSOS9TROrOpGDUWc847d/Uw=="], + "@lifi/intent": ["@lifi/intent@0.0.4", "", { "dependencies": { "borsh": "^2.0.0", "ky": "^1.12.0", "viem": "~2.45.1" } }, "sha512-T9wJGAY6sW6JcunEusXIvehxZcg2pRkaK0b+PUpSje2234yfZSmmcUPS1QTRxB6Iq6XROYopl6aUBIqzTHznew=="], "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="], @@ -649,7 +649,7 @@ "bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], - "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + "borsh": ["borsh@2.0.0", "", {}, "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -1497,6 +1497,8 @@ "@solana/web3.js/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@solana/web3.js/borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -1537,8 +1539,6 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "borsh/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1709,8 +1709,6 @@ "@walletconnect/utils/ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], - "borsh/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1813,8 +1811,6 @@ "@solana/web3.js/bs58/base-x/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "borsh/bs58/base-x/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index 8c9dd76..e3b0aaa 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.6", "@electric-sql/pglite": "^0.3.15", - "@lifi/intent": "0.0.3-alpha.1", + "@lifi/intent": "0.0.4", "@metamask/sdk": "^0.34.0", "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", diff --git a/src/lib/libraries/flowProgress.ts b/src/lib/libraries/flowProgress.ts index c4e59a3..ccdfb29 100644 --- a/src/lib/libraries/flowProgress.ts +++ b/src/lib/libraries/flowProgress.ts @@ -15,7 +15,7 @@ import { hashStruct, keccak256 } from "viem"; import { compactTypes } from "@lifi/intent"; import { getOutputHash, encodeMandateOutput } from "@lifi/intent"; import { addressToBytes32, bytes32ToAddress } from "@lifi/intent"; -import { orderToIntent } from "@lifi/intent"; +import { containerToIntent } from "$lib/utils/intent"; import { getOrFetchRpc } from "$lib/libraries/rpcCache"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; import store from "$lib/state.svelte"; @@ -128,7 +128,7 @@ async function isOutputValidatedOnChain( async function isInputChainFinalised(chainId: bigint, container: OrderContainer) { const { order, inputSettler } = container; const inputChainClient = getClient(chainId); - const intent = orderToIntent(container); + const intent = containerToIntent(container); const orderId = intent.orderId(); if ( @@ -185,7 +185,7 @@ export async function getOrderProgressChecks( fillTransactions: Record ): Promise { try { - const intent = orderToIntent(orderContainer); + const intent = containerToIntent(orderContainer); const orderId = intent.orderId(); const inputChains = intent.inputChains(); const outputs = orderContainer.order.outputs; diff --git a/src/lib/libraries/intentExecution.ts b/src/lib/libraries/intentExecution.ts index c09e07f..373ab7d 100644 --- a/src/lib/libraries/intentExecution.ts +++ b/src/lib/libraries/intentExecution.ts @@ -16,7 +16,7 @@ import { import { compact_type_hash } from "@lifi/intent"; import { addressToBytes32 } from "@lifi/intent"; import { signMultichainCompact, signStandardCompact } from "@lifi/intent"; -import { MultichainOrderIntent, StandardOrderIntent } from "@lifi/intent"; +import { MultichainOrderIntent, StandardEVMIntent as StandardOrderIntent } from "@lifi/intent"; import type { NoSignature, Signature } from "@lifi/intent"; import type { TypedDataSigner } from "@lifi/intent"; import { switchWalletChain } from "$lib/utils/walletClientRuntime"; diff --git a/src/lib/libraries/intentFactory.ts b/src/lib/libraries/intentFactory.ts index 2e7f194..d9ab410 100644 --- a/src/lib/libraries/intentFactory.ts +++ b/src/lib/libraries/intentFactory.ts @@ -16,14 +16,26 @@ import type { Signature, StandardOrder } from "@lifi/intent"; +import { + Intent, + IntentApi, + StandardSolanaIntent, + SOLANA_MAINNET_CHAIN_ID, + SOLANA_TESTNET_CHAIN_ID, + SOLANA_DEVNET_CHAIN_ID +} from "@lifi/intent"; import type { AppCreateIntentOptions, AppTokenContext } from "$lib/appTypes"; import { ERC20_ABI } from "$lib/abi/erc20"; -import { Intent } from "@lifi/intent"; -import { IntentApi } from "@lifi/intent"; import { store } from "$lib/state.svelte"; import { depositAndRegisterCompact, openEscrowIntent, signIntentCompact } from "./intentExecution"; import { intentDeps } from "./coreDeps"; +const SOLANA_CHAIN_IDS = new Set([ + SOLANA_MAINNET_CHAIN_ID, + SOLANA_TESTNET_CHAIN_ID, + SOLANA_DEVNET_CHAIN_ID +]); + const SAME_CHAIN_DURATION_SECONDS = 10 * 60; // 10 minutes const SAME_CHAIN_EXCLUSIVITY_SECONDS = 12 * 3; // 36 seconds @@ -55,12 +67,14 @@ function applyExclusivityOverride( } function toCoreTokenContext(input: AppTokenContext): TokenContext { + const chainId = BigInt(input.token.chainId); return { token: { address: input.token.address, name: input.token.name, - chainId: BigInt(input.token.chainId), - decimals: input.token.decimals + chainId, + decimals: input.token.decimals, + chainNamespace: SOLANA_CHAIN_IDS.has(chainId) ? "solana" : "eip155" }, amount: input.amount }; @@ -75,6 +89,7 @@ function toCoreCreateIntentOptions(opts: AppCreateIntentOptions): CreateIntentOp outputTokens: opts.outputTokens.map(toCoreTokenContext), verifier: opts.verifier, account, + outputRecipient: opts.outputRecipient, lock: { type: "compact", resetPeriod: opts.lock.resetPeriod, @@ -89,6 +104,7 @@ function toCoreCreateIntentOptions(opts: AppCreateIntentOptions): CreateIntentOp outputTokens: opts.outputTokens.map(toCoreTokenContext), verifier: opts.verifier, account, + outputRecipient: opts.outputRecipient, lock: { type: "escrow" } @@ -161,6 +177,8 @@ export class IntentFactory { applySameChainTimings(intentInstance); const sameChain = intentInstance.isSameChain(); const intent = intentInstance.order(); + if (intent instanceof StandardSolanaIntent) + throw new Error("Compact signing is not supported for Solana intents."); applyExclusivityOverride(intent, opts.exclusiveFor, sameChain); const sponsorSignature = await signIntentCompact(intent, account(), this.walletClient); @@ -203,6 +221,8 @@ export class IntentFactory { applySameChainTimings(intentInstance2); const sameChain2 = intentInstance2.isSameChain(); const intent = intentInstance2.singlechain(); + if (intent instanceof StandardSolanaIntent) + throw new Error("Compact deposit and register is not supported for Solana intents."); applyExclusivityOverride(intent, opts.exclusiveFor, sameChain2); if (this.preHook) await this.preHook(inputTokens[0].token.chainId); @@ -242,6 +262,8 @@ export class IntentFactory { applySameChainTimings(intentInstance3); const sameChain3 = intentInstance3.isSameChain(); const intent = intentInstance3.order(); + if (intent instanceof StandardSolanaIntent) + throw new Error("openEscrowIntent is not supported for Solana intents."); applyExclusivityOverride(intent, opts.exclusiveFor, sameChain3); const inputChain = inputTokens[0].token.chainId; diff --git a/src/lib/libraries/intentList.ts b/src/lib/libraries/intentList.ts index d0c3d54..3630fc7 100644 --- a/src/lib/libraries/intentList.ts +++ b/src/lib/libraries/intentList.ts @@ -7,8 +7,8 @@ import { MULTICHAIN_INPUT_SETTLER_ESCROW, MULTICHAIN_INPUT_SETTLER_COMPACT } from "../config"; -import { orderToIntent } from "@lifi/intent"; import { bytes32ToAddress, idToToken } from "@lifi/intent"; +import { containerToIntent } from "$lib/utils/intent"; import type { OrderContainer, StandardOrder, MultichainOrder } from "@lifi/intent"; import { validateOrderContainerWithReason } from "@lifi/intent"; import { orderValidationDeps } from "./coreDeps"; @@ -201,7 +201,7 @@ function getContextDetails(orderContainer: OrderContainer): ContextDetails { export function buildBaseIntentRow(orderContainer: OrderContainer): BaseIntentRow { const order = orderContainer.order; - const orderId = orderToIntent(orderContainer).orderId(); + const orderId = containerToIntent(orderContainer).orderId(); const inputChipsRaw = getInputs(order); const outputChipsRaw = getOutputs(order); const chainScope = getChainScope(order); diff --git a/src/lib/libraries/solver.ts b/src/lib/libraries/solver.ts index 961b7dd..3035562 100644 --- a/src/lib/libraries/solver.ts +++ b/src/lib/libraries/solver.ts @@ -1,12 +1,12 @@ import { BYTES32_ZERO, COIN_FILLER, getChain, getClient, getOracle, type WC } from "$lib/config"; import { hashStruct, maxUint256, parseEventLogs } from "viem"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; -import { addressToBytes32, bytes32ToAddress } from "@lifi/intent"; +import { addressToBytes32, bytes32ToAddress, StandardSolanaIntent } from "@lifi/intent"; import axios from "axios"; import { POLYMER_ORACLE_ABI } from "$lib/abi/polymeroracle"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import { ERC20_ABI } from "$lib/abi/erc20"; -import { orderToIntent } from "@lifi/intent"; +import { containerToIntent } from "$lib/utils/intent"; import { compactTypes } from "@lifi/intent"; import store from "$lib/state.svelte"; import { finaliseIntent } from "./intentExecution"; @@ -66,7 +66,7 @@ export class Solver { orderContainer: { order, inputSettler }, outputs } = args; - const orderId = orderToIntent({ order, inputSettler }).orderId(); + const orderId = containerToIntent(args.orderContainer).orderId(); const outputChainId = Number(outputs[0].chainId); const outputChain = getChain(outputChainId); @@ -310,10 +310,9 @@ export class Solver { const { preHook, postHook, account } = opts; const { orderContainer, fillTransactionHashes, sourceChainId } = args; const { order, inputSettler } = orderContainer; - const intent = orderToIntent({ - inputSettler, - order - }); + const intent = containerToIntent(orderContainer); + if (intent instanceof StandardSolanaIntent) + throw new Error("Finalise is not supported for Solana input intents."); if (fillTransactionHashes.length !== order.outputs.length) { throw new Error( `Fill transaction hash count (${fillTransactionHashes.length}) does not match output count (${order.outputs.length}).` diff --git a/src/lib/screens/FillIntent.svelte b/src/lib/screens/FillIntent.svelte index 018d95a..11907e9 100644 --- a/src/lib/screens/FillIntent.svelte +++ b/src/lib/screens/FillIntent.svelte @@ -11,7 +11,7 @@ import ChainActionRow from "$lib/components/ui/ChainActionRow.svelte"; import TokenAmountChip from "$lib/components/ui/TokenAmountChip.svelte"; import store from "$lib/state.svelte"; - import { orderToIntent } from "@lifi/intent"; + import { containerToIntent } from "$lib/utils/intent"; import { compactTypes } from "@lifi/intent"; import { hashStruct } from "viem"; @@ -110,7 +110,7 @@ $effect(() => { refreshValidation; - const orderId = orderToIntent(orderContainer).orderId(); + const orderId = containerToIntent(orderContainer).orderId(); if (autoScrolledOrderId === orderId) return; const outputs = sortOutputsByChain(orderContainer).flatMap(([, chainOutputs]) => chainOutputs); diff --git a/src/lib/screens/Finalise.svelte b/src/lib/screens/Finalise.svelte index e3ec236..87a5f5d 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -22,7 +22,7 @@ import { SETTLER_ESCROW_ABI } from "$lib/abi/escrow"; import { idToToken } from "@lifi/intent"; import store from "$lib/state.svelte"; - import { orderToIntent } from "@lifi/intent"; + import { containerToIntent } from "$lib/utils/intent"; import { hashStruct } from "viem"; import { compactTypes } from "@lifi/intent"; @@ -41,7 +41,7 @@ let refreshClaimed = $state(0); let claimedByChain = $state>({}); let claimStatusRun = 0; - const inputChains = $derived(orderToIntent(orderContainer).inputChains()); + const inputChains = $derived(containerToIntent(orderContainer).inputChains()); const getInputsForChain = (container: OrderContainer, inputChain: bigint): [bigint, bigint][] => { const { order } = container; if ("originChainId" in order) { @@ -89,7 +89,7 @@ const { order, inputSettler } = container; const inputChainClient = getClient(chainId); - const intent = orderToIntent(container); + const intent = containerToIntent(container); const orderId = intent.orderId(); // Determine the order type. if ( diff --git a/src/lib/screens/IntentList.svelte b/src/lib/screens/IntentList.svelte index 4f2c3a1..f216c90 100644 --- a/src/lib/screens/IntentList.svelte +++ b/src/lib/screens/IntentList.svelte @@ -1,7 +1,7 @@ diff --git a/src/lib/screens/IssueIntent.svelte b/src/lib/screens/IssueIntent.svelte index 96f0914..d038bb1 100644 --- a/src/lib/screens/IssueIntent.svelte +++ b/src/lib/screens/IssueIntent.svelte @@ -34,12 +34,16 @@ const resolveExclusiveFor = (value: string): `0x${string}` | undefined => isAddress(value, { strict: false }) ? value : undefined; + const resolveRecipient = (value: string): `0x${string}` | undefined => + isAddress(value, { strict: false }) ? value : undefined; + const intentOptions = $derived.by( (): AppCreateIntentOptions => ({ exclusiveFor: resolveExclusiveFor(store.exclusiveFor), inputTokens: store.inputTokens, outputTokens: store.outputTokens, verifier: store.verifier, + outputRecipient: resolveRecipient(store.recipient), lock: store.intentType === "compact" ? { @@ -293,6 +297,19 @@
+
+ Recipient + 0 && !resolveRecipient(store.recipient) + ? "error" + : "default"} + bind:value={store.recipient} + /> +
Verifier {#if sameChain} @@ -330,15 +347,7 @@
- {#if !true} - - {:else if !allowanceCheck} + {#if !allowanceCheck} {#snippet name()} Set allowance diff --git a/src/lib/screens/ReceiveMessage.svelte b/src/lib/screens/ReceiveMessage.svelte index c867b68..3d2d430 100644 --- a/src/lib/screens/ReceiveMessage.svelte +++ b/src/lib/screens/ReceiveMessage.svelte @@ -12,7 +12,7 @@ import ChainActionRow from "$lib/components/ui/ChainActionRow.svelte"; import TokenAmountChip from "$lib/components/ui/TokenAmountChip.svelte"; import store from "$lib/state.svelte"; - import { orderToIntent } from "@lifi/intent"; + import { containerToIntent } from "$lib/utils/intent"; import { compactTypes } from "@lifi/intent"; // This script needs to be updated to be able to fetch the associated events of fills. Currently, this presents an issue since it can only fill single outputs. @@ -90,11 +90,11 @@ // const validations = $derived( // orderContainer.order.outputs.map((output) => { - // return orderToIntent(orderContainer) + // return containerToIntent(orderContainer) // .inputChains() // .map((inputChain) => { // return isValidated( - // orderToIntent(orderContainer).orderId(), + // containerToIntent(orderContainer).orderId(), // inputChain, // orderContainer, // output, @@ -110,7 +110,7 @@ $effect(() => { refreshValidation; - const intent = orderToIntent(orderContainer); + const intent = containerToIntent(orderContainer); const orderId = intent.orderId(); if (autoScrolledOrderId === orderId) return; @@ -161,7 +161,7 @@ description="Click on each output and wait until they turn green. Polymer does not support batch validation. Continue to the right." >
- {#each orderToIntent(orderContainer).inputChains() as inputChain} + {#each containerToIntent(orderContainer).inputChains() as inputChain} {#snippet action()} diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts index e6ce9ba..c24fabb 100644 --- a/src/lib/state.svelte.ts +++ b/src/lib/state.svelte.ts @@ -25,7 +25,7 @@ import { tokens as tokensTable } from "./schema"; import { and, eq, ne, notInArray } from "drizzle-orm"; -import { orderToIntent } from "@lifi/intent"; +import { containerToIntent } from "./utils/intent"; import { getOrFetchRpc, invalidateRpcPrefix } from "./libraries/rpcCache"; import { getCurrentConnection, @@ -60,7 +60,7 @@ class Store { async saveOrderToDb(order: OrderContainer) { if (!browser) return; if (!db) await initDb(); - const orderId = orderToIntent(order).orderId(); + const orderId = containerToIntent(order).orderId(); const now = Math.floor(Date.now() / 1000); const id = (order as any).id ?? generateUUID(); const intentType = (order as any).intentType ?? "escrow"; @@ -99,7 +99,7 @@ class Store { console.warn("saveOrderToDb db write failed", { orderId, error }); } } - const idx = this.orders.findIndex((o) => orderToIntent(o).orderId() === orderId); + const idx = this.orders.findIndex((o) => containerToIntent(o).orderId() === orderId); if (idx >= 0) this.orders[idx] = order; else this.orders.push(order); } @@ -285,6 +285,7 @@ class Store { allocatorId = $state(ALWAYS_OK_ALLOCATOR); verifier = $state("polymer"); exclusiveFor: string = $state(""); + recipient: string = $state(""); useExclusiveForQuoteRequest = $state(false); invalidateWalletReadCache(scope: "all" | "balance" | "allowance" | "compact" = "all") { diff --git a/src/lib/utils/intent.ts b/src/lib/utils/intent.ts new file mode 100644 index 0000000..8d2f266 --- /dev/null +++ b/src/lib/utils/intent.ts @@ -0,0 +1,29 @@ +import { + orderToIntent, + SOLANA_MAINNET_CHAIN_ID, + SOLANA_TESTNET_CHAIN_ID, + SOLANA_DEVNET_CHAIN_ID, + StandardEVMIntent, + StandardSolanaIntent, + MultichainOrderIntent +} from "@lifi/intent"; +import type { OrderContainer } from "@lifi/intent"; + +const SOLANA_CHAIN_IDS = new Set([ + SOLANA_MAINNET_CHAIN_ID, + SOLANA_TESTNET_CHAIN_ID, + SOLANA_DEVNET_CHAIN_ID +]); + +export function containerToIntent( + container: OrderContainer +): StandardEVMIntent | StandardSolanaIntent | MultichainOrderIntent { + const { inputSettler, order } = container; + if (!("originChainId" in order)) { + return orderToIntent({ namespace: "eip155", inputSettler, order }); + } + if (SOLANA_CHAIN_IDS.has(order.originChainId)) { + return orderToIntent({ namespace: "solana", inputSettler, order }); + } + return orderToIntent({ namespace: "eip155", inputSettler, order }); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ac2492e..2f0a900 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -12,7 +12,7 @@ import ConnectWallet from "$lib/screens/ConnectWallet.svelte"; import FlowStepTracker from "$lib/components/ui/FlowStepTracker.svelte"; import store from "$lib/state.svelte"; - import { orderToIntent } from "@lifi/intent"; + import { containerToIntent } from "$lib/utils/intent"; // Fix bigint so we can json serialize it: (BigInt.prototype as any).toJSON = function () { @@ -70,8 +70,8 @@ const orderContainer = { ...order, allocatorSignature, sponsorSignature }; // Deduplicate: only add if not already present - const orderId = orderToIntent(orderContainer).orderId(); - const alreadyExists = store.orders.some((o) => orderToIntent(o).orderId() === orderId); + const orderId = containerToIntent(orderContainer).orderId(); + const alreadyExists = store.orders.some((o) => containerToIntent(o).orderId() === orderId); if (alreadyExists) return; store.orders.push(orderContainer); @@ -103,18 +103,18 @@ let scrollStepProgress = $state(0); async function importOrderById(orderId: `0x${string}`): Promise<"inserted" | "updated"> { const importedOrder = await intentApi.getOrderByOnChainOrderId(orderId); - const importedOrderId = orderToIntent(importedOrder).orderId(); + const importedOrderId = containerToIntent(importedOrder).orderId(); const existingIndex = store.orders.findIndex( - (o) => orderToIntent(o).orderId() === importedOrderId + (o) => containerToIntent(o).orderId() === importedOrderId ); await store.saveOrderToDb(importedOrder); selectedOrder = - store.orders.find((o) => orderToIntent(o).orderId() === importedOrderId) ?? importedOrder; + store.orders.find((o) => containerToIntent(o).orderId() === importedOrderId) ?? importedOrder; return existingIndex >= 0 ? "updated" : "inserted"; } async function deleteOrderById(orderId: `0x${string}`): Promise { await store.deleteOrderFromDb(orderId); - if (selectedOrder && orderToIntent(selectedOrder).orderId() === orderId) { + if (selectedOrder && containerToIntent(selectedOrder).orderId() === orderId) { selectedOrder = undefined; } } diff --git a/tests/unit/recipientField.test.ts b/tests/unit/recipientField.test.ts new file mode 100644 index 0000000..73d3edd --- /dev/null +++ b/tests/unit/recipientField.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "bun:test"; +import { isAddress } from "viem"; + +// Mirrors the resolveRecipient helper in IssueIntent.svelte +const resolveRecipient = (value: string): `0x${string}` | undefined => + isAddress(value, { strict: false }) ? (value as `0x${string}`) : undefined; + +describe("resolveRecipient", () => { + it("returns the address for a valid checksummed EVM address", () => { + const addr = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + expect(resolveRecipient(addr)).toBe(addr); + }); + + it("returns the address for a valid lowercase EVM address (strict: false)", () => { + const addr = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + expect(resolveRecipient(addr)).toBe(addr); + }); + + it("returns undefined for an empty string", () => { + expect(resolveRecipient("")).toBeUndefined(); + }); + + it("returns undefined for a partial address", () => { + expect(resolveRecipient("0x1234")).toBeUndefined(); + }); + + it("returns undefined for arbitrary non-address text", () => { + expect(resolveRecipient("alice.eth")).toBeUndefined(); + }); + + it("returns undefined for a hex string that is too long", () => { + expect(resolveRecipient("0x" + "a".repeat(42))).toBeUndefined(); + }); +}); + +describe("outputRecipient in AppCreateIntentOptions", () => { + it("is undefined when recipient field is empty", () => { + const recipient = ""; + const outputRecipient = resolveRecipient(recipient); + expect(outputRecipient).toBeUndefined(); + }); + + it("is set when a valid address is provided", () => { + const recipient = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + const outputRecipient = resolveRecipient(recipient); + expect(outputRecipient).toBe(recipient); + }); + + it("is undefined for an invalid address, so wallet default is used", () => { + const recipient = "not-an-address"; + const outputRecipient = resolveRecipient(recipient); + expect(outputRecipient).toBeUndefined(); + }); +});