From 848fd29237b1c2e79f1d09fd45ab2a0649d8565f Mon Sep 17 00:00:00 2001 From: Asem- Abdelhady Date: Wed, 11 Mar 2026 14:13:54 +0100 Subject: [PATCH 1/8] feat: solana to evm flow --- src/lib/abi/polymer_oracle.json | 392 +++++++++++++++++++++++++ src/lib/components/AwaitButton.svelte | 13 +- src/lib/libraries/flowProgress.ts | 53 +++- src/lib/libraries/solanaFinaliseLib.ts | 245 ++++++++++++++++ src/lib/libraries/solanaValidateLib.ts | 259 ++++++++++++++++ src/lib/libraries/solver.ts | 6 +- src/lib/screens/FillIntent.svelte | 26 +- src/lib/screens/Finalise.svelte | 169 ++++++++++- src/lib/screens/ReceiveMessage.svelte | 237 ++++++++++++--- 9 files changed, 1346 insertions(+), 54 deletions(-) create mode 100644 src/lib/abi/polymer_oracle.json create mode 100644 src/lib/libraries/solanaFinaliseLib.ts create mode 100644 src/lib/libraries/solanaValidateLib.ts diff --git a/src/lib/abi/polymer_oracle.json b/src/lib/abi/polymer_oracle.json new file mode 100644 index 0000000..7deaa82 --- /dev/null +++ b/src/lib/abi/polymer_oracle.json @@ -0,0 +1,392 @@ +{ + "address": "C2rAFLS6xQ78t18rK5s9madY9fztbhTaHwShgYtzonk7", + "metadata": { + "name": "polymer", + "version": "0.0.0", + "spec": "0.1.0" + }, + "instructions": [ + { + "name": "initialize", + "docs": [ + "Initializes the `Polymer` program. Creates \"Polymer\" PDA and sets the `polymer_prover_id`.", + "\"Polymer\" PDA is used sign and attest for the messages received." + ], + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], + "accounts": [ + { + "name": "deployer", + "writable": true, + "signer": true + }, + { + "name": "chain_id" + }, + { + "name": "oracle_polymer", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "polymer_prover_id", + "type": "pubkey" + } + ] + }, + { + "name": "receive", + "docs": [ + "Receive attestation proofs of output settlers from a remote oracle and register them", + "locally by creating attestation accounts." + ], + "discriminator": [86, 17, 255, 171, 17, 17, 187, 219], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "polymer_prover_program" + }, + { + "name": "chain_mapping" + }, + { + "name": "cache_account", + "docs": ["This is the same PDA that was automatically created during load_proof"], + "writable": true + }, + { + "name": "internal", + "writable": true + }, + { + "name": "result_account", + "writable": true + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "proofs", + "type": { + "vec": "bytes" + } + } + ] + }, + { + "name": "set_chain_mapping", + "docs": [ + "Configure the `protocol_chain_id` that corresponds to the given Polymer chain identifier.", + "chain_id is the real chain identifier used by the oracle implementation.", + "Can only be called by the owner of the PolymerOracle.", + "Each mapping may only be set once." + ], + "discriminator": [174, 145, 44, 171, 203, 101, 230, 130], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "chain_mapping", + "writable": true + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "protocol_chain_id", + "type": "u128" + }, + { + "name": "chain_id", + "type": "u128" + } + ] + }, + { + "name": "submit", + "docs": [ + "Checks the fill descriptions and the local attestations.", + "Emits log in the form of `Prove: program: {0x...}, Application: {0x...}, PayloadHash: {0x...}`." + ], + "discriminator": [88, 166, 102, 181, 162, 127, 170, 48], + "accounts": [ + { + "name": "submitter", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + } + ], + "args": [ + { + "name": "source", + "type": "pubkey" + }, + { + "name": "fill_descriptions", + "type": { + "vec": "bytes" + } + } + ] + } + ], + "accounts": [ + { + "name": "ChainId", + "discriminator": [203, 146, 201, 161, 230, 71, 157, 218] + }, + { + "name": "ChainMapping", + "discriminator": [81, 78, 77, 209, 44, 206, 85, 169] + }, + { + "name": "OraclePolymer", + "discriminator": [30, 219, 153, 176, 126, 163, 80, 138] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidChainId", + "msg": "Invalid chain id" + }, + { + "code": 6001, + "name": "InvalidProof", + "msg": "Invalid proof" + }, + { + "code": 6002, + "name": "Unauthorized", + "msg": "Unauthorized mapping creation" + }, + { + "code": 6003, + "name": "CouldNotDecodeSolver", + "msg": "Could not decode solver" + }, + { + "code": 6004, + "name": "CouldNotDecodeTimestamp", + "msg": "Could not decode timestamp" + }, + { + "code": 6005, + "name": "CouldNotDecodeMandateOutputFieldsTuple", + "msg": "Could not decode mandate output fields tuple" + }, + { + "code": 6006, + "name": "CouldNotDecodeOracle", + "msg": "Could not decode oracle" + }, + { + "code": 6007, + "name": "CouldNotDecodeSettler", + "msg": "Could not decode settler" + }, + { + "code": 6008, + "name": "CouldNotDecodeChainId", + "msg": "Could not decode chain id" + }, + { + "code": 6009, + "name": "CouldNotDecodeToken", + "msg": "Could not decode token" + }, + { + "code": 6010, + "name": "CouldNotDecodeAmount", + "msg": "Could not decode amount" + }, + { + "code": 6011, + "name": "CouldNotDecodeRecipient", + "msg": "Could not decode recipient" + }, + { + "code": 6012, + "name": "CouldNotDecodeCallbackData", + "msg": "Could not decode callback data" + }, + { + "code": 6013, + "name": "CouldNotDecodeContext", + "msg": "Could not decode context" + }, + { + "code": 6014, + "name": "CouldNotDecodeFinalAmount", + "msg": "Could not decode final amount" + }, + { + "code": 6015, + "name": "CouldNotConvertU256ToU64", + "msg": "Could not convert u128 to u64" + }, + { + "code": 6016, + "name": "CouldNotConvertU256ToU128", + "msg": "Could not convert u256 to u128" + }, + { + "code": 6017, + "name": "CouldNotConvertU256ToU32", + "msg": "Could not convert u256 to u32" + }, + { + "code": 6018, + "name": "NoAttestationsProvided", + "msg": "No attestations provided" + }, + { + "code": 6019, + "name": "NotCorrectEncodedTypes", + "msg": "Not correct encoded types" + } + ], + "types": [ + { + "name": "ChainId", + "docs": ["Type used to store the chain id of the chain."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "ChainMapping", + "docs": [ + "Type used to map a protocol chain id to a real chain id.", + "The protocol chain id is given as seed." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "OraclePolymer", + "docs": [ + "The PDA to be created that will sign the attestations", + "`polymer_prover_id` is the program id of the polymer prover program.", + "`owner` is the owner of the PolymerOracle. The only address to create mappings.", + "`bump` is the bump of this account to be reused." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "polymer_prover_id", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ] +} diff --git a/src/lib/components/AwaitButton.svelte b/src/lib/components/AwaitButton.svelte index 44f0b59..b2fb093 100644 --- a/src/lib/components/AwaitButton.svelte +++ b/src/lib/components/AwaitButton.svelte @@ -5,6 +5,7 @@ name, awaiting, buttonFunction, + disabled = false, size = "md", variant = "default", fullWidth = false, @@ -15,6 +16,7 @@ name: Snippet; awaiting: Snippet; buttonFunction: () => Promise; + disabled?: boolean; size?: "sm" | "md"; variant?: "default" | "success" | "warning" | "muted"; fullWidth?: boolean; @@ -48,6 +50,7 @@ ]; let buttonPromise: Promise | undefined = $state(); const run = () => { + if (disabled) return; buttonPromise = buttonFunction().catch((error) => { console.error("AwaitButton action failed", error); throw error; @@ -67,7 +70,10 @@ @@ -75,7 +81,10 @@ diff --git a/src/lib/libraries/flowProgress.ts b/src/lib/libraries/flowProgress.ts index a0a6f71..d13f4f9 100644 --- a/src/lib/libraries/flowProgress.ts +++ b/src/lib/libraries/flowProgress.ts @@ -5,24 +5,27 @@ import { INPUT_SETTLER_ESCROW_LIFI, MULTICHAIN_INPUT_SETTLER_COMPACT, MULTICHAIN_INPUT_SETTLER_ESCROW, - getClient + getClient, + solanaDevnetConnection } from "$lib/config"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import { POLYMER_ORACLE_ABI } from "$lib/abi/polymeroracle"; import { SETTLER_ESCROW_ABI } from "$lib/abi/escrow"; import { COMPACT_ABI } from "$lib/abi/compact"; -import { hashStruct, keccak256 } from "viem"; +import { hashStruct, keccak256, parseEventLogs } 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 { getOrFetchRpc } from "$lib/libraries/rpcCache"; +import { deriveAttestationPda } from "$lib/libraries/solanaValidateLib"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; import store from "$lib/state.svelte"; const PROGRESS_TTL_MS = 30_000; const OrderStatus_Claimed = 2; const OrderStatus_Refunded = 3; +const SOLANA_DEVNET_CHAIN_ID = 1151111081099712n; export type FlowCheckState = { allFilled: boolean; @@ -93,6 +96,52 @@ async function isOutputValidatedOnChain( .catch((error) => console.warn("saveTransactionReceipt error", error)); } + if (inputChain === SOLANA_DEVNET_CHAIN_ID) { + return getOrFetchRpc( + `progress:solana-proven:${orderId}:${inputChain.toString()}:${outputKey}:${fillTransactionHash}`, + async () => { + const outputClient = getClient(output.chainId); + const logs = parseEventLogs({ + abi: COIN_FILLER_ABI, + eventName: "OutputFilled", + logs: (receipt as { logs: unknown[] }).logs + }); + const expectedHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: output + }); + const matchingLog = logs.find((log) => { + const logOutputHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: log.args.output + }); + return logOutputHash === expectedHash; + }); + if (!matchingLog) return false; + const solverBytes32 = matchingLog.args.solver as `0x${string}`; + const fillTimestamp = + typeof matchingLog.args.timestamp === "number" + ? matchingLog.args.timestamp + : Number(matchingLog.args.timestamp); + const attestationPda = await deriveAttestationPda({ + evmChainId: output.chainId, + output, + proofOutput: matchingLog.args.output as MandateOutput, + orderId, + fillTimestamp, + solverBytes32, + emittingContract: matchingLog.address as `0x${string}` + }); + const { PublicKey } = await import("@solana/web3.js"); + const info = await solanaDevnetConnection.getAccountInfo(new PublicKey(attestationPda)); + return info !== null; + }, + { ttlMs: PROGRESS_TTL_MS } + ); + } + const block = await getOrFetchRpc( `progress:block:${output.chainId.toString()}:${receipt.blockHash}`, async () => { diff --git a/src/lib/libraries/solanaFinaliseLib.ts b/src/lib/libraries/solanaFinaliseLib.ts new file mode 100644 index 0000000..6c7b41d --- /dev/null +++ b/src/lib/libraries/solanaFinaliseLib.ts @@ -0,0 +1,245 @@ +import { keccak256 } from "viem"; +import idl from "../abi/input_settler_escrow.json"; +import { + SOLANA_INPUT_SETTLER_ESCROW, + SOLANA_INTENTS_PROTOCOL, + SOLANA_POLYMER_ORACLE +} from "../config"; +import type { MandateOutput, SolanaStandardOrder } from "@lifi/intent"; + +/** Convert a 0x-prefixed hex string (32 bytes) to a number[] */ +function hexToBytes32(hex: `0x${string}`): number[] { + return Array.from(Buffer.from(hex.slice(2), "hex")); +} + +/** Convert a bigint to a 32-byte big-endian number[] */ +function bigintToBeBytes32(n: bigint): number[] { + return Array.from(Buffer.from(n.toString(16).padStart(64, "0"), "hex")); +} + +/** + * Derive the order_context PDA for a SolanaStandardOrder. + * orderId = keccak256(borsh(order)); seeds = [b"order_context", orderId] + * This can be used to check if the order has been finalised (PDA closed = finalised). + */ +export async function deriveOrderContextPda( + order: SolanaStandardOrder, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connection: any +): Promise { + const { AnchorProvider, BN, Program } = await import("@coral-xyz/anchor"); + const { PublicKey } = await import("@solana/web3.js"); + + const inputSettlerProgramId = new PublicKey(SOLANA_INPUT_SETTLER_ESCROW); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + + const [polymerOraclePda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + const inputMint = new PublicKey(Buffer.from(order.input.token.slice(2), "hex")); + const userPubkey = new PublicKey(Buffer.from(order.user.slice(2), "hex")); + + // Dummy provider for encoding only (no wallet needed) + const dummyWallet = { + publicKey: userPubkey, + signTransaction: async (tx: unknown) => tx, + signAllTransactions: async (txs: unknown[]) => txs + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedIdl = idl as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(connection, dummyWallet as any, { commitment: "confirmed" }); + const program = new Program(typedIdl, provider); + + const anchorOrder = { + user: userPubkey, + nonce: new BN(order.nonce.toString()), + originChainId: new BN(order.originChainId.toString()), + expires: order.expires, + fillDeadline: order.fillDeadline, + inputOracle: polymerOraclePda, + input: { token: inputMint, amount: new BN(order.input.amount.toString()) }, + outputs: order.outputs.map((o: MandateOutput) => ({ + oracle: hexToBytes32(o.oracle), + settler: hexToBytes32(o.settler), + chainId: bigintToBeBytes32(o.chainId), + token: hexToBytes32(o.token), + amount: bigintToBeBytes32(o.amount), + recipient: hexToBytes32(o.recipient), + callbackData: + o.callbackData === "0x" ? Buffer.alloc(0) : Buffer.from(o.callbackData.slice(2), "hex"), + context: o.context === "0x" ? Buffer.alloc(0) : Buffer.from(o.context.slice(2), "hex") + })) + }; + + const valueForEncoding = { + ...anchorOrder, + outputs: anchorOrder.outputs.map((o) => ({ + ...o, + callbackData: o.callbackData ?? Buffer.alloc(0), + context: o.context ?? Buffer.alloc(0) + })) + }; + let encoded: Uint8Array; + try { + encoded = program.coder.types.encode("standardOrder", valueForEncoding); + } catch { + encoded = program.coder.types.encode("StandardOrder", valueForEncoding); + } + const orderIdHex = keccak256(encoded); + const orderId = Buffer.from(orderIdHex.slice(2), "hex"); + + const [orderContextPda] = PublicKey.findProgramAddressSync( + [Buffer.from("order_context"), orderId], + inputSettlerProgramId + ); + + return orderContextPda.toBase58(); +} + +/** + * Call input_settler_escrow.finalise() on Solana devnet. + * + * @param order The SolanaStandardOrder that was previously opened + * @param solveParams One entry per output: solver = 32-byte Solana pubkey of the filler, + * timestamp = EVM fill block timestamp + * @param attestationPdas Base58 pubkeys of attestation PDAs (one per output) + * @param solanaPublicKey Base58-encoded Solana solver public key (signer + token destination) + * @param walletAdapter Connected Solana wallet adapter + * @param connection Solana Connection instance + */ +export async function finaliseSolanaEscrow(params: { + order: SolanaStandardOrder; + solveParams: { solver: number[]; timestamp: number }[]; + attestationPdas: string[]; + solanaPublicKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + walletAdapter: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connection: any; +}): Promise { + const { AnchorProvider, BN, Program } = await import("@coral-xyz/anchor"); + const { PublicKey, SystemProgram, Transaction } = await import("@solana/web3.js"); + const { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } = + await import("@solana/spl-token"); + + const { order, solveParams, attestationPdas, solanaPublicKey, walletAdapter, connection } = + params; + + const solverPubkey = new PublicKey(solanaPublicKey); + const inputSettlerProgramId = new PublicKey(SOLANA_INPUT_SETTLER_ESCROW); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const intentsProtocolId = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + const anchorWallet = { + publicKey: solverPubkey, + signTransaction: (tx: unknown) => walletAdapter.signTransaction(tx), + signAllTransactions: (txs: unknown[]) => walletAdapter.signAllTransactions(txs) + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedIdl = idl as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(connection, anchorWallet as any, { commitment: "confirmed" }); + const program = new Program(typedIdl, provider); + + const [polymerOraclePda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + + const inputMint = new PublicKey(Buffer.from(order.input.token.slice(2), "hex")); + const userPubkey = new PublicKey(Buffer.from(order.user.slice(2), "hex")); + + const anchorOrder = { + user: userPubkey, + nonce: new BN(order.nonce.toString()), + originChainId: new BN(order.originChainId.toString()), + expires: order.expires, + fillDeadline: order.fillDeadline, + inputOracle: polymerOraclePda, + input: { token: inputMint, amount: new BN(order.input.amount.toString()) }, + outputs: order.outputs.map((o: MandateOutput) => ({ + oracle: hexToBytes32(o.oracle), + settler: hexToBytes32(o.settler), + chainId: bigintToBeBytes32(o.chainId), + token: hexToBytes32(o.token), + amount: bigintToBeBytes32(o.amount), + recipient: hexToBytes32(o.recipient), + callbackData: + o.callbackData === "0x" ? Buffer.alloc(0) : Buffer.from(o.callbackData.slice(2), "hex"), + context: o.context === "0x" ? Buffer.alloc(0) : Buffer.from(o.context.slice(2), "hex") + })) + }; + + // Compute orderId = keccak256(borsh(anchorOrder)) + const valueForEncoding = { + ...anchorOrder, + outputs: anchorOrder.outputs.map((o) => ({ + ...o, + callbackData: o.callbackData ?? Buffer.alloc(0), + context: o.context ?? Buffer.alloc(0) + })) + }; + let encoded: Uint8Array; + try { + encoded = program.coder.types.encode("standardOrder", valueForEncoding); + } catch { + encoded = program.coder.types.encode("StandardOrder", valueForEncoding); + } + const orderIdHex = keccak256(encoded); + const orderId = Buffer.from(orderIdHex.slice(2), "hex"); + + // Derive PDAs + const [inputSettlerEscrowPda] = PublicKey.findProgramAddressSync( + [Buffer.from("input_settler_escrow")], + inputSettlerProgramId + ); + const [orderContext] = PublicKey.findProgramAddressSync( + [Buffer.from("order_context"), orderId], + inputSettlerProgramId + ); + + // ATAs: destination = solver, orderContext = escrow + const destinationTokenAccount = getAssociatedTokenAddressSync(inputMint, solverPubkey, false); + const orderPdaTokenAccount = getAssociatedTokenAddressSync(inputMint, orderContext, true); + + const remainingAccounts = attestationPdas.map((pda) => ({ + pubkey: new PublicKey(pda), + isWritable: false, + isSigner: false + })); + + const ix = await program.methods + .finalise(anchorOrder as any, solveParams as any) + .accounts({ + solver: solverPubkey, + inputSettlerEscrow: inputSettlerEscrowPda, + user: userPubkey, + destination: solverPubkey, + destinationTokenAccount, + orderContext, + orderPdaTokenAccount, + mint: inputMint, + intentsProtocolProgram: intentsProtocolId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId + } as any) + .remainingAccounts(remainingAccounts) + .instruction(); + + // The on-chain finalise flow closes escrow/state accounts to `user`, so `user` + // must be writable even though the generated IDL marks it readonly. + for (const key of ix.keys) { + if (key.pubkey.equals(userPubkey)) key.isWritable = true; + } + + const tx = new Transaction().add(ix); + tx.feePayer = solverPubkey; + tx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash; + + const signature = await provider.sendAndConfirm(tx, [], { commitment: "confirmed" }); + + return signature; +} diff --git a/src/lib/libraries/solanaValidateLib.ts b/src/lib/libraries/solanaValidateLib.ts new file mode 100644 index 0000000..9d771d1 --- /dev/null +++ b/src/lib/libraries/solanaValidateLib.ts @@ -0,0 +1,259 @@ +import axios from "axios"; +import { keccak256 } from "viem"; +import idl from "../abi/polymer_oracle.json"; +import { SOLANA_INTENTS_PROTOCOL, SOLANA_POLYMER_ORACLE } from "../config"; +import type { MandateOutput } from "@lifi/intent"; + +const POLYMER_PROVER_PROGRAM = "CdvSq48QUukYuMczgZAVNZrwcHNshBdtqrjW26sQiGPs"; + +/** Convert a bigint to a 16-byte little-endian Buffer (u128 LE) */ +function u128ToLeBytes(n: bigint): Buffer { + const buf = Buffer.alloc(16); + const bn = BigInt(n); + buf.writeBigUInt64LE(bn & 0xffffffffffffffffn, 0); + buf.writeBigUInt64LE(bn >> 64n, 8); + return buf; +} + +function normalizeBytes32Hex(value: `0x${string}`): Buffer { + return Buffer.from(value.slice(2), "hex"); +} + +function normalizeEvmIdentifier( + value: `0x${string}` | undefined, + fallbackBytes32: `0x${string}` +): Buffer { + if (!value) return normalizeBytes32Hex(fallbackBytes32); + const hex = value.slice(2); + if (hex.length === 40) return Buffer.from(hex.padStart(64, "0"), "hex"); + if (hex.length === 64) return Buffer.from(hex, "hex"); + throw new Error(`Invalid EVM identifier length: ${value.length}`); +} + +/** Encode the common payload for a MandateOutput */ +export function encodeCommonPayload(output: MandateOutput): Buffer { + const token = Buffer.from(output.token.slice(2), "hex"); + const amountHex = BigInt(output.amount).toString(16).padStart(64, "0"); + const amount = Buffer.from(amountHex, "hex"); + const recipient = Buffer.from(output.recipient.slice(2), "hex"); + const callbackData = + output.callbackData === "0x" + ? Buffer.alloc(0) + : Buffer.from(output.callbackData.slice(2), "hex"); + const context = + output.context === "0x" ? Buffer.alloc(0) : Buffer.from(output.context.slice(2), "hex"); + const callLen = Buffer.alloc(2); + callLen.writeUInt16BE(callbackData.length, 0); + const ctxLen = Buffer.alloc(2); + ctxLen.writeUInt16BE(context.length, 0); + return Buffer.concat([token, amount, recipient, callLen, callbackData, ctxLen, context]); +} + +/** Encode fill description: solver(32) || orderId(32) || timestamp(4,BE) || commonPayload */ +export function encodeFillDescription( + solverBytes32: `0x${string}`, + orderId: `0x${string}`, + timestamp: number, + commonPayload: Buffer +): Buffer { + const solver = Buffer.from(solverBytes32.slice(2), "hex"); + const orderIdBytes = Buffer.from(orderId.slice(2), "hex"); + const ts = Buffer.alloc(4); + ts.writeUInt32BE(timestamp >>> 0, 0); + return Buffer.concat([solver, orderIdBytes, ts, commonPayload]); +} + +/** + * Derive the attestation PDA for a given fill. + * Seeds: [b"attestation", oracle_polymer_pda, evmChainId_le16, output.oracle, output.settler, payloadHash] + * Program: SOLANA_INTENTS_PROTOCOL + */ +export async function deriveAttestationPda(params: { + evmChainId: bigint; + output: MandateOutput; + proofOutput?: MandateOutput; + orderId: `0x${string}`; + fillTimestamp: number; + solverBytes32: `0x${string}`; + emittingContract?: `0x${string}`; +}): Promise { + const { PublicKey } = await import("@solana/web3.js"); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const intentsProtocol = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + const [oraclePolymerPda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + + const payloadOutput = params.proofOutput ?? params.output; + const chainIdSeed = u128ToLeBytes(params.evmChainId); + const commonPayload = encodeCommonPayload(payloadOutput); + const fillDesc = encodeFillDescription( + params.solverBytes32, + params.orderId, + params.fillTimestamp, + commonPayload + ); + const payloadHash = Buffer.from(keccak256(fillDesc).slice(2), "hex"); + const source = normalizeBytes32Hex(payloadOutput.oracle); + const application = normalizeEvmIdentifier(params.emittingContract, payloadOutput.settler); + + const [attestationPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("attestation"), + oraclePolymerPda.toBuffer(), + chainIdSeed, + source, + application, + payloadHash + ], + intentsProtocol + ); + + return attestationPda.toBase58(); +} + +/** + * Submit a Polymer proof to the Solana oracle_polymer.receive() instruction. + * Fetches the proof from the /polymer API (hex-encoded) then calls oracle_polymer.receive(). + */ +export async function submitProofToSolanaOracle(params: { + evmChainId: bigint; + output: MandateOutput; + proofOutput?: MandateOutput; + orderId: `0x${string}`; + fillTimestamp: number; + solverBytes32: `0x${string}`; + emittingContract?: `0x${string}`; + fillBlockNumber: number; + globalLogIndex: number; + mainnet: boolean; + solanaPublicKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + walletAdapter: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connection: any; +}): Promise { + const { AnchorProvider, Program } = await import("@coral-xyz/anchor"); + const { PublicKey, SystemProgram, ComputeBudgetProgram } = await import("@solana/web3.js"); + + const signerPubkey = new PublicKey(params.solanaPublicKey); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const polymerProverProgramId = new PublicKey(POLYMER_PROVER_PROGRAM); + const intentsProtocolId = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + // Fetch Polymer proof via /polymer route (returns hex-encoded bytes) + let proof: string | undefined; + let polymerIndex: number | undefined; + for (const waitMs of [0, 2000, 4000, 8000, 16000]) { + if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs)); + const response = await axios.post( + "/polymer", + { + srcChainId: Number(params.evmChainId), + srcBlockNumber: params.fillBlockNumber, + globalLogIndex: params.globalLogIndex, + polymerIndex, + mainnet: params.mainnet + }, + { timeout: 15_000 } + ); + const dat = response.data as { proof: string | undefined; polymerIndex: number }; + polymerIndex = dat.polymerIndex; + if (dat.proof) { + proof = dat.proof; + break; + } + } + if (!proof) { + throw new Error("Polymer proof unavailable. Try again after the fill attestation is indexed."); + } + + // Derive PDAs + const [oraclePolymerPda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + const chainIdSeed = u128ToLeBytes(params.evmChainId); + + const [chainMapping] = PublicKey.findProgramAddressSync( + [Buffer.from("chain_mapping"), oraclePolymerPda.toBuffer(), chainIdSeed], + intentsProtocolId + ); + const [cacheAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("cache"), signerPubkey.toBuffer()], + polymerProverProgramId + ); + const [internalAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("internal")], + polymerProverProgramId + ); + const [resultAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("result"), signerPubkey.toBuffer()], + polymerProverProgramId + ); + + // Attestation PDA (goes into remaining_accounts as writable) + const payloadOutput = params.proofOutput ?? params.output; + const commonPayload = encodeCommonPayload(payloadOutput); + const fillDesc = encodeFillDescription( + params.solverBytes32, + params.orderId, + params.fillTimestamp, + commonPayload + ); + const payloadHash = Buffer.from(keccak256(fillDesc).slice(2), "hex"); + const source = normalizeBytes32Hex(payloadOutput.oracle); + const emittingContractApplication = normalizeEvmIdentifier( + params.emittingContract, + payloadOutput.settler + ); + + const [attestationPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("attestation"), + oraclePolymerPda.toBuffer(), + chainIdSeed, + source, + emittingContractApplication, + payloadHash + ], + intentsProtocolId + ); + + // Build Anchor program + const anchorWallet = { + publicKey: signerPubkey, + signTransaction: (tx: unknown) => params.walletAdapter.signTransaction(tx), + signAllTransactions: (txs: unknown[]) => params.walletAdapter.signAllTransactions(txs) + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedIdl = idl as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(params.connection, anchorWallet as any, { + commitment: "confirmed" + }); + const program = new Program(typedIdl, provider); + + const proofBytes = Buffer.from(proof, "hex"); + // Increase compute budget for chained CPI into IntentsProtocol + const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }); + + return await program.methods + .receive([proofBytes] as any) + .accounts({ + signer: signerPubkey, + oraclePolymer: oraclePolymerPda, + polymerProverProgram: polymerProverProgramId, + chainMapping, + cacheAccount, + internal: internalAccount, + resultAccount, + intentsProtocolProgram: intentsProtocolId, + systemProgram: SystemProgram.programId + } as any) + .remainingAccounts([{ pubkey: attestationPda, isWritable: true, isSigner: false }]) + .preInstructions([computeIx]) + .rpc({ commitment: "confirmed" }); +} diff --git a/src/lib/libraries/solver.ts b/src/lib/libraries/solver.ts index d371b19..35e8300 100644 --- a/src/lib/libraries/solver.ts +++ b/src/lib/libraries/solver.ts @@ -53,6 +53,7 @@ export class Solver { args: { orderContainer: OrderContainer; outputs: MandateOutput[]; + solverBytes32?: `0x${string}`; // override default addressToBytes32(account()) }, opts: { preHook?: (chainId: number) => Promise; @@ -64,7 +65,8 @@ export class Solver { const { preHook, postHook, account } = opts; const { orderContainer: { order, inputSettler }, - outputs + outputs, + solverBytes32 } = args; const orderId = orderToIntent({ order, inputSettler }).orderId(); @@ -122,7 +124,7 @@ export class Solver { value, abi: COIN_FILLER_ABI, functionName: "fillOrderOutputs", - args: [orderId, outputs, order.fillDeadline, addressToBytes32(account())] + args: [orderId, outputs, order.fillDeadline, solverBytes32 ?? addressToBytes32(account())] }); const fillReceipt = await getClient(outputChain.id).waitForTransactionReceipt({ hash: transactionHash diff --git a/src/lib/screens/FillIntent.svelte b/src/lib/screens/FillIntent.svelte index 2147aa1..bb88f8c 100644 --- a/src/lib/screens/FillIntent.svelte +++ b/src/lib/screens/FillIntent.svelte @@ -3,6 +3,7 @@ import { bytes32ToAddress } from "@lifi/intent"; import { getOutputHash } from "@lifi/intent"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; + import { solanaAddressToBytes32, isValidSolanaAddress } from "$lib/utils/solana"; import { Solver } from "$lib/libraries/solver"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import AwaitButton from "$lib/components/AwaitButton.svelte"; @@ -29,6 +30,10 @@ account: () => `0x${string}`; } = $props(); + // Solana→EVM: order has no `inputs` field (SolanaStandardOrder) + const isSolanaToEvm = $derived(!("inputs" in orderContainer.order)); + let solanaSolverAddress = $state(""); + let refreshValidation = $state(0); let autoScrolledOrderId = $state<`0x${string}` | null>(null); let fillRun = 0; @@ -123,6 +128,21 @@ description="Fill each chain once and continue to the right. If you refreshed the page provide your fill tx hash in the input box." >
+ {#if isSolanaToEvm} + +
+ + +
+
+ {/if} {#each sortOutputsByChain(orderContainer) as chainIdAndOutputs} @@ -148,7 +168,11 @@ store.walletClient, { orderContainer, - outputs: chainIdAndOutputs[1] + outputs: chainIdAndOutputs[1], + solverBytes32: + isSolanaToEvm && isValidSolanaAddress(solanaSolverAddress) + ? solanaAddressToBytes32(solanaSolverAddress) + : undefined }, { preHook, diff --git a/src/lib/screens/Finalise.svelte b/src/lib/screens/Finalise.svelte index d8e2e81..0ddf7d2 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -1,12 +1,15 @@ @@ -160,6 +161,14 @@ > Fill + {:else if isSolanaToEvm && !isValidSolanaAddress(solanaSolverAddress)} + {:else} v == BYTES32_ZERO) ? "default" : "muted"} @@ -178,7 +187,6 @@ }, { preHook, - postHook: postHookScroll, account } ) diff --git a/src/lib/screens/ReceiveMessage.svelte b/src/lib/screens/ReceiveMessage.svelte index 2731e47..5436ec9 100644 --- a/src/lib/screens/ReceiveMessage.svelte +++ b/src/lib/screens/ReceiveMessage.svelte @@ -189,11 +189,21 @@ /** * Returns a button function for submitting a Polymer proof to the Solana oracle (Solana→EVM). */ - function solanaValidateButtonFn(output: MandateOutput, fillTransactionHash: `0x${string}`) { + function solanaValidateButtonFn(output: MandateOutput) { return async () => { if (!solanaWallet.connected || !solanaWallet.publicKey) { throw new Error("Connect your Solana wallet first"); } + const fillTransactionHash = store.fillTransactions[outputKey(output)]; + if ( + !fillTransactionHash || + !fillTransactionHash.startsWith("0x") || + fillTransactionHash.length !== 66 + ) { + throw new Error( + "Fill transaction hash not available. Please wait for the fill to be recorded." + ); + } const outputClient = getClient(output.chainId); const receipt = await outputClient.getTransactionReceipt({ hash: fillTransactionHash }); const logs = parseEventLogs({ @@ -360,7 +370,7 @@ buttonFunction={status ? async () => {} : isSolanaToEvm - ? solanaValidateButtonFn(output, fillTxHash as `0x${string}`) + ? solanaValidateButtonFn(output) : Solver.validate( store.walletClient, {